Skip to main content

State tree

The state tree is an incremental Merkle tree (LeanIMT, depth 22) that holds every leaf ever inserted into the pool: commitment leaves (COMMITMENT_LEAF_TAG) and nullifier leaves (NULLIFIER_LEAF_TAG). Witnesses for transact proofs reference it, while ragequit verifies commitment existence via the on-chain commitments mapping rather than a state-tree circuit witness.

Properties

  • Depth 22: the tree is 22 levels deep, which supports up to 2^22 (4,194,304) leaves in total. Commitment and nullifier leaves share that space, and each transact inserts both kinds, so the capacity is sized for years of activity.
  • LeanIMT structure: the LeanIMT structure keeps on-chain insertion cheap because an insert re-hashes only the insertion path rather than the whole tree. PoolVault maintains the tree.
  • Leaves are timestamped: what actually gets inserted is Poseidon(COMMITMENT_LEAF_TAG, commitment, block.timestamp), not the bare commitment, which defends against state-tree replay if a contract were ever forked.
  • Root tracked on-chain: the current root is tracked on-chain, and circuits verify membership against a published root.

Why timestamped leaves matter

The createdAtBlock field on every Note is not a block number, despite its name. It stores the on-chain block.timestamp at insertion. The SDK uses it when recomputing the leaf for state-proof witnesses, so a note imported with a wrong createdAtBlock fails state-proof lookups with "Leaf X not found in tree" when you try to spend it.

Implications for SDK callers

  • After a deposit or transact, the SDK's resolveCommitmentTimestamp utility polls PoolVault.commitments(commitment) to capture the correct timestamp.
  • An out-of-band note must carry its createdAtBlock field, because the recipient needs the timestamp to spend it.
  • Allow for reorg windows when polling: a fresh transaction may briefly read 0 from commitments(...) before the storage write propagates, so poll with retries.