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.
PoolVaultmaintains 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
resolveCommitmentTimestamputility pollsPoolVault.commitments(commitment)to capture the correct timestamp. - An out-of-band note must carry its
createdAtBlockfield, 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.