ProtocolCore release 0.1.0

Nullifiers

Domain-separated one-time spend markers prevent replaying a Shielded Note.

A proof must let the program reject a second spend without revealing the spend secret or input commitment. Lethenymous derives a nullifier from the pool, asset, spend secret and note randomness:

N=Poseidon9(NULLIFIER_DOMAIN,poolhi,poollo,assethi,assetlo,secrethi,secretlo,randomnesshi,randomnesslo)N = Poseidon9(NULLIFIER\_DOMAIN, pool_{hi}, pool_{lo}, asset_{hi}, asset_{lo}, secret_{hi}, secret_{lo}, randomness_{hi}, randomness_{lo})

The proof binds this value to the privately reconstructed note. When a note is spent, the nullifier is public and permanently recorded as a spent marker.

Canonical spent PDA

Core derives the spent account using the seeds:

["spent", pool_pubkey, nullifier_32_bytes]

The program performs the canonical PDA bump search itself. If the account already contains a valid SpentNullifier for that pool and value, the instruction fails with NullifierSpent. The account is never overwritten. An alternate bump cannot create another identity for the same spend.

The fee payer funds the PDA's rent when it is first initialized. With a relayer, this can be the relayer's SOL rather than the private trader's wallet.

Replay behavior

  1. The proof establishes that the nullifier is the one derived from the hidden input note and secret.
  2. Core verifies the proof and accepted root.
  3. Core initializes the one canonical spent PDA.
  4. A later transaction carrying the same nullifier fails before it can settle.

The relayer may preflight the PDA and has a short-lived in-memory replay lease, but these are defense-in-depth. On-chain nullifier state is the authoritative replay control.

The nullifier domain is 0x5a4b43504d4e0003; all protocol domains are listed in Constants & domains.

PreviousShielded Note modelNext Merkle tree
Source baseline: frozen Core v0.1.0 / SDK v0.1.1.