05

V0.5 Testnet · Open → Noid

Mask (deposit)

Masking is the door from Open mode into Noid mode: public coins go into the pool and become private notes. In V0.5 the wallet proves the deposit itself, and can pre-pay the relayer with a fee note in the same step.

Sent by

Your wallet

Proof

deposit (5 signals)

Gas

Paid by you

Creates

1 or 2 notes

Noid mode

builds C1 and C2

proves the deposit

proof + 2 notes

Your address

sends deposit()

with the coins

coins + transaction

NoidPool

verifies the proof

inserts the notes

No relayer: a mask is public by nature — the chain sees your address send the coins.

In the wallet

  1. 1You choose the amount a and, optionally, a relayer fee f.
  2. 2Picks fresh randomness r1, r2, and reads your uc (registered[you]) and the relayer’s rc from the pool.
  3. 3C1 = Poseidon(1, a − f, r1, uc) is your note; C2 = Poseidon(1, f, r2, rc) is the relayer’s fee note.
  4. 4Encrypts {a − f, r1} to your encPub and {f, r2} to the relayer’s.
  5. 5Proves (a − f) + f = a — about 20 seconds in the extension.

Your address

  1. 6Sends deposit(proof, C1, C2, encryptedNote1, encryptedNote2) with the a coins attached.

Inside NoidPool

  1. 7Requires coins attached and a non-zero C1; refuses a commitment that already exists (“change r value”).
  2. 8Sets c2Enabled from whether C2 is zero.
  3. 9Builds the 5 public signals itself — injecting its own relayerCommitment — and verifies the proof.
  4. 10Inserts the commitments into the current pool’s tree, pushes one new root, and emits NoteCreated for each.
  5. 11The coins simply stay in the contract; from here on, ownership is tracked by commitments alone.

What the deposit proof proves

Circuit, simplified
c1 === Poseidon(1, a1, r1, uc1)                      // your note commits to (a1, r1, uc1)
c2_enabled · (c2 − Poseidon(1, a2, r2, uc2)) === 0   // the fee note — only if enabled
c2_enabled · (1 − c2_enabled) === 0                  // c2_enabled is 0 or 1
depositAmount === a1 + c2_enabled · a2               // nothing created, nothing lost
#Public signalMeaning
0depositAmountmsg.value — the public amount
1c1your commitment
2c2the relayer’s fee commitment (0 if none)
3c2_enabled1 if the fee note exists, else 0
4uc2the relayer’s commitment — supplied by the contract
  • uc2 is public and comes from the contract, so a fee note can’t be pointed anywhere but the relayer.
  • uc1 is private and unconstrained — which is exactly what lets you deposit straight to someone else.
  • No fee note? Pass C2 = 0: the circuit zeroes the fee, depositAmount must equal a1, and one NoteCreated is emitted instead of two.

The Merkle tree

Each pool is an incremental Merkle tree of depth 20 — 1,048,576 leaves — and when one fills, the contract opens the next. Appending costs 20 hashes up a single path, thanks to filledSubtrees (the latest left node at each level), and a batch pushes one root, not one per leaf. Each pool remembers its last 10 roots.

Go deeperThe Aptos exception — the tree updates in a second step

Aptos verifies Groth16 in pure Move, and proof verification plus a Merkle insertion runs past its execution limit. So on Aptos, mask, send and unmask only queue their commitments, and pool::update_root then inserts each one with a fourth circuit, new_root, which proves the new root follows from the old one.

new_root
public inputs : oldSubtreesHash, commitment, leafIndex
public outputs: newRoot, newSubtreesHash
private       : oldSubtrees[20]   // the relayer's copy of filled_subtrees

The pool refuses new operations while anything is queued — a proof against a root that is about to change would prove against state that never existed — and the relayer drains the queue as part of the normal flow. Everything else is identical: an Aptos note is the same object as a Monad note, it just becomes spendable one transaction later.

On-chain, anyone can see

  • Your address, the total amount, and when

Stays private

  • How the amount splits between your note and the fee note
  • When your note is spent, and where its value goes