TESTNET
ACTIVE
v1.0

DEVELOPER GUIDE

Technical implementation details and cryptographic primitives

CRYPTOGRAPHIC PRIMITIVES

Cryptographic Primitives

KEY DERIVATION HIERARCHY

// Master Keys
nullifying_key = Poseidon(spending_key, 1)
MPK = Poseidon(spending_key, nullifying_key)
// Viewing Keys (for note encryption/decryption)
viewing_private_key = X25519(SHA256(spending_key))
viewing_public_key = X25519.publicKey(viewing_private_key)
// Note Creation
NSK = Poseidon(MPK, random)
commitment = Poseidon(NSK, token, value)
// Spending (Double-Spend Prevention)
nullifier = Poseidon(nullifying_key, leaf_index)

KEY PROPERTIES

  • spending_key: Root secret — must be kept private and backed up. It is the only way to recover your account; losing it means permanent loss of all shielded funds.
  • nullifying_key: Derived from spending_key — must be kept private. Exposure allows an attacker to generate your nullifiers and front-run your spends.
  • MPK: Master Public Key — safe to share. Senders need your MPK to create a note commitment addressed to you.
  • viewing_private_key: Derived via X25519(SHA256(spending_key)) — used locally to decrypt incoming notes. Never leave the client.
  • viewing_public_key: Safe to share. Senders need your VPK to encrypt the note payload so only you can read it. Together with MPK, it forms your transfer address.
  • NSK: Note Secret Key — unique per note, derived from MPK and a random value.
  • commitment: Public commitment to a note, stored in the Merkle tree.
  • nullifier: Unique identifier for a spent note, prevents double-spending.

SENDING A TRANSFER

To send tokens to another user, you need their MPK and viewing_public_key (VPK). MPK is used to derive the output note commitment; VPK is used to encrypt the note payload via ECDH so only the recipient can decrypt it. Both values together act as the recipient's shielded address.

▲ SHIELD OPERATION

Deposit tokens into the privacy pool by creating an encrypted note commitment

PROCESS FLOW

1

Generate Note Parameters

Create random value for NSK derivation, compute NSK = Poseidon(MPK, random)

2

Compute Commitment

commitment = Poseidon(NSK, token_id, amount)

3

Encrypt Note Data

Encrypt (NSK, token, value, random) using ChaCha20-Poly1305 with recipient's viewing key

4

Submit to Pool

Call pool::shield(pool, coin, commitment, encrypted_note) - NO ZK PROOF REQUIRED

RESULT

  • Commitment added to Merkle tree
  • Encrypted note stored on-chain via event
  • User's public balance decreases
  • Shielded balance increases (only visible to user)

▼ UNSHIELD OPERATION

Withdraw tokens from the privacy pool using a zero-knowledge proof of ownership

PROCESS FLOW

1

Select Input Note

Choose an unspent note with sufficient balance for withdrawal

2

Compute Nullifier

nullifier = Poseidon(nullifying_key, leaf_index) - prevents double-spending

3

Create Change Note (if needed)

If input_value > withdraw_amount, create change commitment for remaining balance

4

Generate ZK Proof

Prove: (1) ownership of note, (2) note exists in Merkle tree, (3) correct nullifier, (4) balance conservation, (5) correct change commitment

5

Submit to Pool

Call pool::unshield(pool, proof, public_inputs, recipient, encrypted_change_note)

RESULT

  • ZK proof verified on-chain
  • Nullifier marked as spent (prevents double-spending)
  • Tokens transferred to recipient address
  • Change note added to Merkle tree (if any)
  • No link between deposit and withdrawal revealed

⇄ TRANSFER OPERATION

Privately transfer tokens using 2 inputs to generate a recipient note and optional change, completely hiding sender, receiver, and value from observers.

PROCESS FLOW

1

Select Input Notes

Choose up to 2 unspent notes with sufficient total balance. If only 1 note needed, use a dummy note.

2

Create Output Notes

Output 1: Recipient's note with transfer amount

Output 2: Sender's change note with remaining balance

3

Compute Nullifiers

Generate nullifier for each input note to mark them as spent

4

Generate ZK Proof

Prove: (1) ownership of input notes, (2) inputs exist in Merkle tree, (3) correct nullifiers, (4) balance conservation (in = out), (5) valid output commitments

5

Submit to Pool

Call pool::transfer(pool, proof, public_inputs, encrypted_notes)

RESULT

  • ZK proof verified on-chain
  • Input nullifiers marked as spent
  • Output commitments added to Merkle tree
  • Sender, recipient, and amount remain hidden
  • Only recipient can decrypt their note using viewing key

⇌ SWAP OPERATION

Exchange tokens privately via DeepBook V3, spending shielded input notes and receiving shielded output notes

PROCESS FLOW

1

Select Input Notes

Choose up to 2 unspent notes of the input token with sufficient total balance

2

Compute Swap Data Hash

Hash swap parameters (token_in, token_out, amount_in, min_amount_out) to bind them to the proof

3

Create Output Notes

Output note: recipient commitment for the swapped token_out amount

Change note: sender's change commitment if input exceeds amount_in

4

Generate ZK Proof

Prove: (1) ownership of input notes, (2) inputs exist in Merkle tree, (3) correct nullifiers, (4) valid swap_data_hash, (5) valid output commitments

5

Submit to Pool

Call pool::swap(pool_in, pool_out, deepbook_pool, proof, public_inputs, amount_in, min_amount_out, encrypted_output_note, encrypted_change_note)

RESULT

  • ZK proof verified on-chain
  • Input nullifiers marked as spent
  • Swap executed via DeepBook V3
  • Output and change commitments added to Merkle tree
  • Token amounts and identities remain hidden
OCTOPUS

Developed by June

GitHub