Skip to content
Docs / Merkle tree

Inside Ordeo

Each note is a leaf in a single append-only Merkle tree

The pool keeps a Poseidon Merkle tree of depth 24. It stores only what it needs to add the next leaf and remember recent roots; your app rebuilds the full tree from public events.


On this page

Shape

A binary tree of depth 24 with room for 16,777,216 leaves. Each leaf is a note commitment; each inner node is Poseidon2(left, right). Leaves are only ever appended, in order. A shield adds one leaf; every spend adds two, including fillers.

Zero hashes

zeros(0)     = 0
zeros(i + 1) = Poseidon2(zeros(i), zeros(i))

An empty subtree of height i hashes to zeros(i). The pool hard-codes all 25 values; the empty tree’s root is zeros(24), stored as the first root at deployment.

Insertion with filled subtrees

For each level the pool keeps filledSubtrees[level], the last left-hand node written there. To insert, it walks up 24 levels: a current node on the left is paired with the zero hash and remembered; one on the right is paired with the remembered left node. That is 24 Poseidon hashes per leaf and no storage of the full tree.

Root history

Every insertion stores the new root in a 100-slot ring. A proof is accepted if its root is any of the last 100. That gives a proof time to reach the chain while others add notes, but a proof built on a root older than the last 100 insertions has to be redone. Since a spend inserts two leaves, that is about 50 spends. Bundlers refuse a proof whose root has dropped out, and the app re-proves.

When the tree is full

After leaf 16,777,215 the next insertion reverts with TreeFull. Every shield inserts a leaf, and every spend inserts two, so from then on no shield and no spend can succeed on this pool. There is no second tree and no upgrade path; a new pool would have to be deployed. Reaching that point takes millions of transactions, each paying gas.

What the client rebuilds and how it checks

The site’s leaf index reads every NewCommitment and Shielded event from the pool’s deployment block and serves them, in leaf order, in pages of 2,000 at /api/pool/leaves. Everyone gets the same full list, so the server cannot tell which notes any visitor is able to open.

The app rebuilds the tree from those leaves and asks the pool isKnownRoot(root). If the pool does not know the root, the index may simply be a few seconds behind: the app retries up to four times, refetching everything on the last try, and then stops with an error rather than proving against a wrong tree. A wrong or incomplete list is detected, never trusted. Only leaves it has not seen before are decrypted.