Commitment Tree
Every note commitment ever created is a leaf in the on-chain commitment tree, managed by CommitmentTreeLib over LeanIMT. State is a single ERC-7201-slotted struct:
struct CommitmentsTree {
uint256 treeNumber; // active tree
mapping(uint256 => LeanIMTData) merkleTrees; // depth-16 LeanIMTs
mapping(uint256 => mapping(uint256 => bool)) rootHistory; // per tree, every past root
mapping(uint256 => bool) spentNullifiers; // global spent set
}
The tree is append-only: spending never removes a leaf, it only adds a nullifier. See State Transitions for how it mutates.
LeanIMT semantics
A standard fixed-depth tree pads empty leaves with a zero hash. A Lean IMT instead promotes a lone left child: the parent of a node with no right sibling is that node itself. Consequences: smaller membership proofs and cheaper insertions (no zero-hash padding chain).
Multi-tree rollover
Each tree holds at most 2^16 = 65,536 leaves. When the active tree can't fit a batch, insertLeaves starts a new tree and increments treeNumber; each tree keeps its own rootHistory. A single insert batch must fit one tree — count > MAX_LEAVES reverts BatchExceedsTreeCapacity (unreachable under the block gas limit, but a guard against silently overflowing to depth 17 and breaking every membership proof).
Per-input anchors
A spend proves membership against a historical root. In the 2-in/2-out model each input carries its own anchor (so the two inputs may live in different trees):
struct ActionOp {
uint64 treeNumber1; uint256 commitmentRoot1; // input 1 anchor
uint64 treeNumber2; uint256 commitmentRoot2; // input 2 anchor
// …
}