The Anchor Service
The anchor service is the single process that turns commitments into anchors. It holds the anchoring authority keypair, and it is the only component that submits transactions to the commitment program.
It runs one loop:
every ANCHOR_INTERVAL_MS:
for each user with pending (unanchored) leaves:
if the seal policy fires for that user:
anchorUser(userId)
Errors for one user never abort the sweep for the others.
The seal policy
A user's pending leaves are sealed into a batch when either condition holds:
| Condition | Default | Rationale |
|---|---|---|
Pending leaf count ≥ maxLeaves | 64 | Amortize the transaction fee across many memories |
Age of the oldest pending leaf ≥ maxAgeMs | 300 000 ms (5 min) | Bound the worst-case time-to-anchored for a low-volume user |
The policy is a pure function of the pending set and the current time, which is what makes it testable:
shouldSeal(pending, now, { maxLeaves: 64, maxAgeMs: 300_000 }): boolean
The consequence for callers: a memory becomes provable within one sweep interval of the seal condition firing, not immediately on write. An agent that writes one memory and immediately calls verify() will get false. That is correct behavior, not a bug - the memory is stored, it is simply not yet anchored.
Anchoring one user
1. Advance the tree
The user's persisted incremental Merkle tree is loaded and the leaves it has not yet seen are appended - O(log n) each. If no tree is persisted (first anchor) or the persisted leafCount exceeds the actual leaf count (drift), the tree is rebuilt from all leaves. This is the only O(n) path, and it self-heals corruption.
2. Save the tree, optimistically
The save is guarded on the tree's previous leafCount. If another sweep advanced the tree in the meantime, the guard fails, this round is skipped, and the next sweep picks it up. No lock is held across an RPC call.
3. Ensure the commitment account
If the user has no commitment account on-chain, init_user creates the PDA at ["user", owner], funded by the authority, with authority set to the anchoring key. The instruction is idempotent per owner - the PDA can only exist once.
4. Commit, with nonce retry
The service reads the account's current nonce, opens a commit_batches row recording that expectation, and submits:
commit_root(new_root: [u8; 32], new_count: u64, expected_nonce: u64)
The program rejects the transaction with NonceMismatch if a concurrent commit has already advanced the nonce. On that error, the service marks the batch failed, refetches the nonce, and retries - up to 3 attempts, after which it throws and the next sweep tries again.
This is the mechanism that makes concurrent anchoring safe: two writers cannot silently interleave, because the second one's transaction is rejected by the chain rather than accepted out of order.
Batch state machine
| State | Meaning |
|---|---|
pending | Row created; transaction not yet submitted |
submitted | Transaction signature obtained; awaiting confirmation |
confirmed | Confirmed at confirmed commitment. Every leaf in the range is now anchored |
failed | The transaction errored. The leaves remain pending and will be re-sealed |
A failed batch leaves no trace on-chain and no trace on the memories. It is a record that an attempt occurred, retained for operational forensics.
What each batch records
merkleRoot the rolling root over leaves 0..leafRangeEnd
leafRangeStart always 0 - the root is rolling, not incremental
leafRangeEnd the highest leaf index this root covers
memoryCountAfter leafRangeEnd + 1
nonce the on-chain nonce this commit expected
txSignature the anchoring transaction, once submitted
status the state above
leafRangeStart is 0 on every batch. This is not a bug - the root is rolling, so every anchor covers the entire log from index zero.
Cost
One transaction per batch, per user, per sweep in which the policy fires.
| Anchored per transaction | On-chain cost per memory |
|---|---|
| 1 memory (low-volume user, 5-minute timeout) | one base fee |
| 64 memories (seal-on-count) | one base fee ÷ 64 |
The payload is a 32-byte root, an 8-byte count, and an 8-byte nonce. There is no per-memory account, no per-memory rent, and no data growth on-chain as a user's log grows - the account is a fixed 129 bytes forever.
Operational requirements
The nonce guard makes multiple instances safe but not useful: each additional instance loses the race, marks its batch failed, and burns a transaction fee discovering that. Run one.
The authority keypair is the anchoring key. It pays fees and signs commit_root. It cannot decrypt memories, cannot forge a leaf, and cannot regress a count. Its compromise means an attacker can stall anchoring or anchor a wrong root - both detectable by any user who recomputes their own root and compares. Keep it funded, keep it off request-serving processes, and see trust assumptions for what it does and does not confer.
Sweep interval is ANCHOR_INTERVAL_MS, default 300000. Lowering it reduces time-to-anchored for low-volume users and raises transaction count. It does not affect the 64-leaf threshold.
Failure semantics
| Failure | Behavior |
|---|---|
| RPC unreachable | Sweep logs and continues; leaves stay pending; next sweep retries |
| Nonce clash | Refetch and retry, up to 3 times |
| Tree save race | Skip this user this sweep |
| Persisted tree corrupt or drifted | Full rebuild from leaves - self-healing |
| Transaction submitted, confirmation times out | Batch stays submitted; the on-chain nonce has advanced, so the next attempt reads the new nonce and commits the current root, which subsumes the lost batch |
The last row is the important one. Because the root is rolling, a lost or ambiguous anchor is never a data-loss event: the next successful anchor commits a root that already covers every leaf the lost one covered. There is no gap to reconcile.
Next: Available networks.