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
Your address
sends deposit()
with the coins
NoidPool
verifies the proof
inserts the notes
In the wallet
- 1You choose the amount a and, optionally, a relayer fee f.
- 2Picks fresh randomness r1, r2, and reads your uc (registered[you]) and the relayer’s rc from the pool.
- 3C1 = Poseidon(1, a − f, r1, uc) is your note; C2 = Poseidon(1, f, r2, rc) is the relayer’s fee note.
- 4Encrypts {a − f, r1} to your encPub and {f, r2} to the relayer’s.
- 5Proves (a − f) + f = a — about 20 seconds in the extension.
Your address
- 6Sends
deposit(proof, C1, C2, encryptedNote1, encryptedNote2)with the a coins attached.
Inside NoidPool
- 7Requires coins attached and a non-zero C1; refuses a commitment that already exists (“change r value”).
- 8Sets c2Enabled from whether C2 is zero.
- 9Builds the 5 public signals itself — injecting its own relayerCommitment — and verifies the proof.
- 10Inserts the commitments into the current pool’s tree, pushes one new root, and emits NoteCreated for each.
- 11The coins simply stay in the contract; from here on, ownership is tracked by commitments alone.
What the deposit proof proves
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 signal | Meaning |
|---|---|---|
| 0 | depositAmount | msg.value — the public amount |
| 1 | c1 | your commitment |
| 2 | c2 | the relayer’s fee commitment (0 if none) |
| 3 | c2_enabled | 1 if the fee note exists, else 0 |
| 4 | uc2 | the 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.
public inputs : oldSubtreesHash, commitment, leafIndex
public outputs: newRoot, newSubtreesHash
private : oldSubtrees[20] // the relayer's copy of filled_subtreesThe 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
