Skip to main content

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:

FieldWhat it is
valueThe token amount (e.g. 100000000000000000000000 for 100,000 ORB)
asset_idWhich asset it is — 0 is ORB
owner_pk_axx-coordinate of the owner's Baby JubJub public key
blindingA 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.

Proving a note's contents without spending it

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.

Your anonymity set is one tree, not the whole pool

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-agnosticasset_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.