Notes and ownership
MIP410 represents shielded value as individual notes, each specifying an asset, amount, and recipient address. It follows an unspent transaction output (UTXO) model where spending consumes existing notes and creates new ones. All of a note’s contents are fixed when it is created, including the recipient address. A payment consumes its input notes in full and creates a recipient note. If the total input value exceeds the payment amount, the remainder returns to the sender in a newly created change note. The recipient address determines which spending key can authorize use of the note. The circuit checks that the note’s address is associated with the key used to verify the spending authorization. A wallet’s shielded balance comes from the unspent notes it controls. Notes reserved by an initiated but unresolved payment still exist. Their value is unavailable for another spend while the hold is pending.Note contents
A note is defined by a set of private information, known as the note opening. It is used to compute the note’s public representation, called its note commitment. The opening is encoded as eleven field elements, grouped below by purpose.
The reserved elements of the note opening have no active note functionality.
The timestamp carries forward information from the oldest input timestamp. The Shielded Note
reference specifies the field encoding and
commitment order.
The note commitment is the Poseidon2 hash of the encoded opening, commonly
displayed as a hexadecimal string. The fields are hashed in this order:
Merkle tree
MIP410 uses a Merkle tree as a canonical onchain record of note commitments created through successful transactions. Before adding a new commitment, the pool verifies the required proofs and transaction checks, including public backing or conservation of value from spent input notes. When a note is spent, a zero-knowledge proof establishes that its commitment belongs to this tree without revealing which commitment is being used. This proves that the note was recorded through an accepted transaction. The diagram shows part of the tree, with illustrative hexadecimal hash previews:H_CE becomes H_CD,
and the changes propagate up to a new root. Commitments A, B, and C and their
positions remain unchanged, as does H_AB. The pool stores the new root as
part of the transaction’s onchain state changes and the Merkle root commits
to the recorded leaves and their positions.
H_CD, and the sibling hashes
at the omitted levels. If the calculated root matches an accepted root,
the path establishes that the commitment belongs to that tree.
Changing the note details e.g. amount changes its commitment, so the altered
note will fail to reconstruct the recorded root using that path.
Merkle Path
When a note is created, the pool publishes its commitment and permanent position, called its leaf index, in an onchain event. Proving that the note belongs to the tree, such as when spending it, requires the sibling hashes. Together with the leaf index, the sibling hashes form its Merkle path. An offchain tree mirror reconstructs the commitment tree from the pool’s past commitment events and updates it as new commitments are appended. The note’s leaf index identifies its position in that tree, from which the mirror obtains the sibling hashes needed for a proof. One shared tree supplies paths for many notes without replaying the chain history for each transaction or maintaining a separate set of hashes for every note.Nullifiers and spent notes
The commitment tree is an append-only Merkle tree and spending a note does not remove its commitment from the tree. The pool maintains a spent set of nullifiers identifying notes that have already been spent. A nullifier is derived deterministically from the nullifier key and the note’s leaf index, so the same note always produces the same nullifier.DOMAIN_NULLIFIER is a fixed label in the hash calculation, called a domain
tag. Its purpose becomes clear when we introduce input locks below.
The zero-knowledge proof establishes that the nullifier was derived correctly
for the note being spent. At payment acceptance or unshielding, the pool checks
whether the nullifier is already in the spent set. If it is, the transaction
is rejected. Otherwise, once all transaction checks pass, the pool
records the nullifier as spent.
Input locks and availability
Initiation reserves notes without spending them, so the pool needs to track reservations separately from completed spends. The pool records an input lock for each note reserved by an initiated payment. It rejects another initiation or unshielding attempt using a locked note, even though that note has not yet been spent.DOMAIN_LOCK. This produces a separate
identifier that can be published at initiation without revealing the
note’s nullifier.
Rejection or reclaim releases the locks so the notes become available again.
Acceptance leaves the locks marked and records the notes as spent.
Payment effects and hold IDs
The payment effect specifies the state changes to apply if a payment is accepted. It contains the input nullifiers to record as spent and the recipient and change note commitments to append to the tree. At initiation, the sender commits to these values by publishing their hash, called the payment effect hash. This fixes the proposed result while keeping the values hidden until acceptance.DOMAIN_EFFECT is the domain tag for this hash.
The pool uses this hash as the hold ID and records the payment’s input locks,
recipient VASP binding commitment, payload commitment, and deadline under it.
The payload commitment binds the private payment record and identifiers to
this payment. These records let the pool track and resolve the pending payment.
At acceptance, the payment effect is disclosed and the pool checks that its
hash matches the hold ID before applying the state changes. This prevents
acceptance from substituting different input or output notes. Privacy Guarantees explains what observers
can infer from these public records.