Notes & the Merkle Forest
Orbinum's shielded pool does not hold balances. It holds notes — and on-chain, it holds only a hash of each one.
This page is the reference for what a note contains, how it is spent, and where commitments live. Every other page links here rather than restating it.
What a note is
A note is a private record of value, like a bearer bill. It has four fields:
| Field | What it is |
|---|---|
value | The token amount (e.g. 100000000000000000000000 for 100,000 ORB) |
asset_id | Which asset it is — 0 is ORB |
owner_pk_ax | x-coordinate of the owner's Baby JubJub public key |
blinding | A random 32-byte scalar |
The blinding is what makes the other three unguessable. Without it, an observer
could hash every plausible (value, asset_id, owner) triple and find a match.
Commitments: what is actually on-chain
The note itself never touches the chain. What the runtime stores is a commitment — a Poseidon hash of the four fields:
commitment = Poseidon4(value, asset_id, owner_pk_ax, blinding)
The commitment is public and reveals nothing. It is a one-way hash, so anyone can see it and no one can invert it.
Your wallet keeps the four plaintext fields locally, encrypted. That is why the vault stores notes rather than a balance, and why losing them means running a rescan — the chain holds hashes, not your money's details.
A disclosure key encodes the four inputs so a third party can recompute the hash and confirm what the note holds — without gaining any ability to spend it. See Note Disclosure.
Nullifiers: how spending works
A commitment is never deleted. Spending a note instead publishes a nullifier:
nullifier = Poseidon(commitment, spending_key)
The runtime keeps a set of every nullifier it has seen. A second spend of the same note produces the same nullifier, the runtime rejects it, and the double-spend fails.
The privacy property is that the link runs one way. Computing the nullifier requires the spending key, so an observer sees a nullifier appear and cannot tell which commitment it came from. Deposits and spends are not connectable.
The Merkle Forest: where commitments live
Commitments are stored in binary Merkle trees of depth 20. Each tree holds up to 1,048,576 notes; when one fills, it is sealed and a fresh tree opens automatically, so the pool never runs out of room.
The tree is what lets you prove a note exists without saying which one it is. To spend, you generate a Merkle proof of inclusion — the path of sibling hashes from your leaf up to the root. The proof is checked against the root, and the root says nothing about which leaf you used.
Internal nodes are Poseidon hashes of their children:
parent = Poseidon(left_child, right_child)
At depth 20, verifying inclusion costs ~20 hashes regardless of how full the tree is.
Sealing and the forest
When a tree reaches its 1,048,576th leaf, the runtime seals it in the same block as that final insert:
- The tree's final root becomes a permanent anchor. Unlike the rolling ring of recent roots, a sealed root never expires — so a note in a sealed tree stays spendable indefinitely, and a proof generated against it does not go stale.
- The active tree resets to empty and subsequent commitments land in the next tree.
Sealing also reclaims storage. The active tree stores internal Merkle nodes
on-chain, not just leaves, so proofs are served in O(depth) rather than
recomputed from scratch — which means state grows with every commitment, not
only with block count. Once a tree seals, an on_idle sweep prunes ~99.8% of its
internal nodes; a proof for a note in a sealed tree recomputes the pruned
siblings from the leaves on demand. Per-tree storage drops sharply after sealing,
and the sealed root stays a permanent anchor.
Global leaf indices are u32, which caps the forest at roughly 4,096 trees of
depth 20. Your wallet does not need to track any of this: the leaf index
determines the tree, and treeIdOf in @orbinum/protocol resolves it.
A Merkle proof is proof of membership in one tree. The crowd you hide in is the tree holding your note.
In practice this bounds the set at ~1M notes rather than fragmenting it in any surprising way, and in a young pool everyone shares tree 0. But it is a real property: a note in a sparsely-populated freshly-opened tree has a smaller crowd than one in a full sealed tree.
One pool for every asset
Commitments are asset-agnostic — asset_id is a field inside the note, not a
separate tree. Every asset shares the same trees and therefore the same anonymity
set.
The alternative, one tree per asset, would split the crowd: a rarely-used token would have a handful of notes to hide among. Here, a 100 ORB note and a 50 USDT note are indistinguishable commitments in the same tree.
Related
- Privacy Architecture — what the shielded pool protects, and what it does not
- Keys & Identity — where
owner_pkand the spending key come from - How Proofs Work — the circuits that enforce all of the above