Notes
A note is a record of shielded value the protocol tracks for one specific account. Your balance in the pool is never a single number: it is a set of notes, and each note can be spent independently. Deposits create notes, transfers consume notes and create new ones, and withdrawals consume notes to release value back to a public address.
Anatomy of a note
Poseidon(precommitment, label), where precommitment = Poseidon(noteAddressHash, tokenId, value, metadata).Poseidon(precommitment, depositSecret), carried forward through transfers.noteAddressHash = Poseidon(ownerAddress, noteSecret), which flows into the precommitment and commitment. The nullifier is Poseidon(privateNullifyingKey, commitment) and is not derived from noteSecret.Poseidon(ownerAddress, noteSecret). Binds the note to a specific recipient.true for normal notes, false for draft recipient notes the session generated as a sender but hasn't claimed (for example, notes awaiting out-of-band delivery).INACTIVE, PENDING, ACTIVE, SPENT, REJECTED, or EXITED. See NoteManager.block.timestamp when the commitment was inserted, as hex. Despite the name it is a timestamp, not a block number. State-proof witnesses recompute the timestamped leaf from it (see State tree).label = Poseidon(precommitment, depositSecret) at deposit time. It is present only on the note that originated its lineage.Note lifecycle
- Created when a note enters the system through a deposit, as a transact output (either change or a recipient note), or through an import.
INACTIVEfor draft and unclaimed notes the session knows about but cannot yet spend.PENDINGimmediately after creation: a deposit waits for ASP attestation, while a transact output waits for chain inclusion.ACTIVEonce attested or confirmed, at which point the note is spendable.SPENTwhen spending the note through a transact or withdraw publishes its nullifier on-chain.EXITEDafter a successful ragequit.REJECTEDif the ASP refuses to attest, which is recoverable via ragequit.
Valid status transitions
NoteManager.updateNoteStatus enforces VALID_STATUS_TRANSITIONS and throws InvalidStatusTransitionError for any edge not listed below:
| From | Allowed to |
|---|---|
INACTIVE | INACTIVE, PENDING, EXITED |
PENDING | PENDING, ACTIVE, EXITED, REJECTED |
ACTIVE | ACTIVE, SPENT, EXITED, REJECTED |
SPENT | SPENT |
EXITED | EXITED |
REJECTED | REJECTED, EXITED |
When a note transitions to SPENT or EXITED, spentAtBlock is set if it is currently unset: spentAtBlock ??= currentBlock ?? bigintToHex(syncCursor).
Before spending: verify the selected inputs against PoolVault.spentNullifiers(nullifier). This catches notes that are already spent on-chain even when local state is stale after a partial flow. See Failure modes.
Why notes (and not balances)?
Tracking value as notes rather than one balance is what makes the privacy properties work.
- Per-spend privacy: each spend reveals only a fresh nullifier, computed from the spender's private nullifying key and the note's commitment, so the chain learns that some note was spent but not which one.
- Composability: the
transact_NxMcircuit family takes N input notes and produces M outputs, which lets you split and merge value freely. - Selective disclosure: because every note carries its own secret, handing an auditor one note's secret reveals exactly that payment and nothing else (see Selective disclosure).