Skip to main content

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:

ConditionDefaultRationale
Pending leaf count ≥ maxLeaves64Amortize the transaction fee across many memories
Age of the oldest pending leaf ≥ maxAgeMs300 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

StateMeaning
pendingRow created; transaction not yet submitted
submittedTransaction signature obtained; awaiting confirmation
confirmedConfirmed at confirmed commitment. Every leaf in the range is now anchored
failedThe 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 transactionOn-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

Run exactly one anchor service

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

FailureBehavior
RPC unreachableSweep logs and continues; leaves stay pending; next sweep retries
Nonce clashRefetch and retry, up to 3 times
Tree save raceSkip this user this sweep
Persisted tree corrupt or driftedFull rebuild from leaves - self-healing
Transaction submitted, confirmation times outBatch 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.