06

V0.5 Testnet · Noid → Noid

Send (transfer)

A private send moves value between Noid users entirely inside the pool — no coins enter or leave, and neither side is revealed. You paste an ordinary address; the wallet finds its Noid identity on-chain.

Sent by

The relayer

Proof

transfer (19 signals)

Fee

A private note

Notes

up to 4 in → 3 out

Menoid wallet

gathers notes

proves the transfer

Transfer Calls

Relayer

validates locally

submits, pays gas

transfer()

NoidPool

verifies · nullifies

inserts new notes

NoteCreated

Receiver

finds the note

by trial decryption

In the wallet

  1. 1Resolves the receiver’s address to their userCommitment and encryption key, from the chain.
  2. 2Gathers unspent notes until they cover amount + fee.
  3. 3Batches them into groups of at most 4 — the circuit’s fixed input width.
  4. 4Builds up to 3 outputs per batch: C1 to the receiver, C2 your change, C3 the relayer’s fee.
  5. 5For each input, fetches its Merkle path and computes its nullifier. Unused slots are switched off with the enabled[] mask.
  6. 6Proves each batch and hands the Transfer Calls — public signals, nullifiers, proof, output commitments, encrypted notes — to the relayer.

At the relayer

gate 1 — the relayer protecting itself

  1. 7Checks each call, validates the proofs off-chain, and checks the fee is worth it. Invalid or unprofitable → rejected, before any gas is spent.
  2. 8Submits transfer(calls) and pays the gas.

Inside NoidPool

gate 2 — the real one

  1. 9enabled[] is boolean, each pool exists, each root is among its pool’s last 10, and each nullifier is unspent and unique within the call.
  2. 10Rejects duplicate output commitments, builds the 19 public signals — injecting its own relayerCommitment — and verifies each proof.
  3. 11Marks every nullifier spent, inserts the new commitments, and emits NoteCreated for each.

On the receiver’s side

  1. 12Their wallet finds the new note by trial decryption. Nothing announces the payment — they simply find it.

Batches that build on each other

Several Transfer Calls run atomically in one transaction, and a later call may spend a note that an earlier call created in that same transaction. That is note aggregation: fold 4 notes into 1, feed it into the next call with 3 more, and so on — large payments without ever widening the circuit. And since a 1-input and a 4-input transfer produce identically shaped proofs, the proof never leaks how many notes you spent.

What the proof proves

  • You own the notes: sk → spendPk → userCommitment with your address, and every enabled input is built from it.
  • They exist: each enabled input’s Merkle path leads to a root the pool accepts.
  • The nullifiers are right: each is Poseidon(2, commitment, r, sk) — only the owner can make one.
  • The outputs are well-formed and locked to the intended owners.
  • Nothing is created: every amount is range-checked below 2¹²⁸, and the inputs sum to the outputs.
  • The relayer is paid: the third output must belong to the relayer’s commitment.

Why the range checks matter

Proof arithmetic wraps around a huge prime. Without a bound, an output could be a giant number that wraps around and still balances the sum — value from nothing. RangeCheck(128) splits every amount into 128 bits, so it can’t happen.
Go deeperThe 19 public signals
IndexSignalCount
0relayerCommitment1
1–4enabled[]4
5–8roots[]4
9–12nullifiers[]4
13–15output_enabled[]3
16–18c_outs[]3

Everything else — sk, your address, the input commitments, every amount and random number, the Merkle paths, and the receivers — is private.

On-chain, anyone can see

  • That a transfer happened
  • Nullifiers and new commitments
  • The relayer as the sender

Stays private

  • Sender and receiver
  • The amounts
  • Which notes were spent