# State tree

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

The state tree is an incremental Merkle tree (LeanIMT, depth 22) that holds every leaf ever inserted into the pool: [commitment](/concepts/commitments-and-nullifiers) leaves (`COMMITMENT_LEAF_TAG`) and nullifier leaves (`NULLIFIER_LEAF_TAG`). Witnesses for transact proofs reference it, while [ragequit](/operations/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`](/protocol/contracts/pool-vault) 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](/concepts/notes) 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](/concepts/discoverable-vs-oob) 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.
