How a transfer works

One leaf proof is one shielded transfer. Two recursive layers sit above it, and the chain settles the result.

This page is the long form of the mechanism. It ends with what a block publishes, which is the half a privacy claim is worth nothing without.

01 / Keys

Keys, addresses and notes

The split is Monero's

Spending authority is ask = H("qnero/ask", sk). The address half that receives is pk = H(PK, ak, nk). The viewing side is an ML-KEM decapsulation key, dk, which opens the AEAD ciphertext on each output the way a view key opens an ECDH stealth address, and nk carries spent status on its own.

An address qn1... is 2571 characters, most of it the 1568-byte ML-KEM-1024 key. Scanning is the same linear trial decryption of every output a Monero wallet does.

A note, and the tree it lives in

A note commits as cm = H(CM, H(NOTE, pk, rho, r), v). Commitments are appended to a 4-ary Poseidon Merkle tree, and each block header carries the tree root. The anonymity set is the tree: every note ever appended. Ring size 16 with decoys has no equivalent here.

A spend publishes nf = H(NF, nk, rho, r), the nullifier, into UsedNullifiers. The chain refuses a nullifier it has already seen, so a note spends once. A padding input slot publishes a nullifier over no note, under a NF_DUMMY tag, and the two are the same uniform hash in the public record.

02 / The circuit

Up to 2 inputs, exactly 2 outputs, a public fee

From a note to a settled block Five steps left to right: your notes, a leaf proof, a private batch of six leaf proofs, an optional public batch of fifty-three private batches, and settlement on chain. The first four steps are proved locally. Only the last is in the block. ONE TRANSFER, THREE PROVING LAYERS, ONE SETTLEMENT Your notes up to 2 inputs 2 outputs, 1 fee Leaf proof 320 gates 105500 bytes Private batch N = 6, ZK 150908 bytes Public batch n = 53, optional 237544 bytes Settlement pallet-shielded nullifiers + leaves PROVED IN YOUR WALLET, OR BY AN AGGREGATOR IN THE BLOCK
The public batch is an aggregator's non-ZK bundle and is optional. A wallet submits a private batch on its own today.

One leaf proof is one shielded transfer: up to 2 input notes, exactly 2 outputs, a public fee, and ct_digest binding the output ciphertexts, as 26 public inputs fixed in qnero_circuit::layout.

Per real input the circuit derives pk from ask and nk, recomputes cm, walks a 16-level Merkle path to the anchored header's zk_tree_root, and emits the nullifier. An input's pk is never witnessed, so a wrong credential yields a commitment absent from the tree. Outputs range-check to 62 bits, so the balance equation cannot wrap the field.

An anchor expires after BlockHashWindow, 256 blocks, which is 8.5 hours at the 120-second target. That is the only waiting rule on this chain.

Two layers above

N leaf proofs make a zero-knowledge private batch, which is the on-chain transaction unit, and n of those make a public batch, an aggregator's non-ZK bundle. Both check their inner proofs against a baked-in verifier key. The chain default is N = 6, and the measured public batch is n = 53, carrying 318 real slots.

Settlement

pallet-shielded settles each real slot: both nullifiers marked used, both commitments appended, ct_digest recomputed and compared. A segment carrying an already-claimed nullifier is skipped and the rest settles, because refusing it would let one participant destroy an aggregator's batch for free. Settlements are unsigned and fee free, so the fee floors carry anti-spam alone.

03 / Your node

What the node you ask learns

Run your own node. Short of that, two rules hold over what a wallet asks, and both are asserted in the test suite.

A sync never names a nullifier. Spent status is decided against a local copy of UsedNullifiers, paged whole through state_getKeysPaged. Probing that map with a wallet's own nullifiers would hand a node, in the clear, the set of values that wallet will publish when it spends.

A spend never names a leaf. Merkle paths are rebuilt locally from the leaf range at the anchor block. The explorer holds the same line: it makes no Merkle proof call at all, and a lint rule fails the build on the spellings of that call it knows. That fence fails open and is a reviewer's aid, so the property rests on the reads the code makes.

A node still sees an IP address, a cadence and the submissions. Traffic analysis stays open. The gap between a spend's anchor block and its inclusion block is public too, and it tracks the speed of the machine that proved it.

A wallet verifies no proof of work, and will not in v1. A RandomX verification wants a 256 MiB cache and has no browser build, so above a wallet's newest checkpoint the node picks every header field. Two per-leaf values are bound to a leaf by nothing on chain: the ciphertext bytes, and where a commitment sits inside its block's own leaf range. A node whose headers all check out can move an incoming payment onto a position this wallet reads as somebody else's, and that payment reads as never arriving. A rescan against a second node is the recovery, and both wallets say so on screen. M13 closes it by taking every read with a trie proof at the pinned block hash, verified against the header's own state root.

04 / Disclosure

What a v1 block reveals

The full list, from docs/CIRCUIT.md section 10.7. A privacy claim is worth what its disclosure list says.

What is published, and what it ties together
Published What it ties together
A shield's payer, its value and the leaf it created The account that paid, the exact amount, and the note it became
A coinbase note's value and its block How much was minted, and when. The recipient is inside an inner the chain cannot open
Every leaf's commitment, and the tree root in each header The shape of the tree and its growth per block
Every settled nullifier That some note was spent. The set does not say which one
Each settlement's slot count and fee How many leaf slots a submission settled and what it paid
Each settled slot's two nullifiers, its two commitments, both leaf indices and both ciphertexts The two notes one spend created, at consecutive leaf indices, publicly siblings and publicly tied to the two nullifiers spent alongside them
A vesting payout's beneficiary and amount A genesis-fixed allocation, the account it went to, and when
A burn's account and amount Value leaving circulation, and the account it left from
A refused call's own arguments Who tried to send what to whom, though nothing moved

The three edges worth stating twice

The entry is the sharp edge. shield is a signed extrinsic, so the payer's account, the amount and the leaf index are all on chain together, and value that enters the pool that way is linked to the account it came from at the moment it enters. What is unlinked is everything after: the note's spends are proofs, and the commitment a shield publishes is the last time that value has a name. A wallet that wants its entry unlinked has to be paid from the pool.

A payment and its change are publicly a pair. The settlement event names which two nullifiers were spent together and which two leaves that spend created. Which of the two leaves is the change stays hidden, because the wallet draws the payment's output slot per spend.

A refused transparent transfer publishes everything. The call filter is checked at dispatch, so a transparent transfer is a valid extrinsic that enters a block, pays its fee and then fails. Its arguments are in the block body and its failure is in the events, permanently. One mistaken attempt publishes exactly the sender, recipient and amount triple the policy exists to deny. A wallet should refuse these calls client-side.

Two smaller leaks are recorded and open: a padding slot publishes zero commitments, so the real-transfer count is public, and a ciphertext of a size other than 1792 bytes stands out.

05 / Numbers

Every figure, and the machine it came from

One workstation, 20 cores, WSL2. docs/BENCH.md is the log. No figure below was measured on a public network, because there is no public network.

Measurements from docs/BENCH.md
Measurement Figure Detail
Leaf proof 105500 bytes 320 gates, 26 public inputs. Prove mean 183 ms over 9 runs, single threaded, range 138 to 381 ms, the spread being 16 grinding bits. Verify 2.2 ms
Private batch, N = 6 150908 bytes 131 felts. Prove 3.34 to 3.61 s on four threads. Verify 4.2 ms
Private batch, N = 7 24530 gates 6.4 s and 2.06 GiB peak natively, 65.8 s and 1.72 GiB in a browser. About 2x the cost of six for one more slot
Public batch, n = 53 237544 bytes 29 s to prove, 318 real slots, a 2.2x margin under MAX_PROOF_BYTES of 524288. Check 6.04 ms
A payment in a browser, one thread 33.6 s 910.4 MiB of peak linear memory, on top of 12.1 s of circuit build once per worker. Natively on one thread the same batch is 9.82 s, so wasm costs about 3.3x
A payment in a browser, four threads 11.2 s Against 37.6 s on one thread in the same sitting, a factor of 3.36. Send pressed to settled block is 24.6 s against 53.4 s. The pool is capped at four threads and needs a cross-origin isolated page
A send on the command line 6.47 to 7.81 s Picks two notes largest-first, rebuilds Merkle paths locally and proves the batch itself
Verification 2.2 to 6.04 ms Flat in what a proof wraps, all native. A private batch verifies in 14.1 ms of browser wasm against 3.7 ms for the same call natively, 3.8x, and the module's first verify costs 20.5 ms because V8 has yet to tier that code up
Address 2571 characters Mostly its 1568-byte ML-KEM-1024 key
Amounts 1010 planck One pool quantum, which is 0.01 QNR. A send defaults to a fee of 8 quanta
Emission 411 quanta a block At genesis supply, against the 120-second target

There is no phone in these figures. A desktop core under headless Chromium is the proxy, and the stated 2-to-4 factor is a floor, because its low end is a peak single-core score ratio that leaves out both throttling and mobile browser engines. With threads shipped the estimate is a band rather than a point: 11.2 s at 4x is 45 s and the slow sitting's 27.2 s at 4x is close to two minutes, four usable cores assumed inside both. The memory fits a 6 GB device. Threads make the arithmetic comfortable at the fast end and leave it open at the slow end, and no device has been measured.

One transfer per submission today: six slots, one filled, so 150908 bytes carries one transfer, and about 22 KB each when full. The testnet targets are a proof per transaction under 105 KB or amortized below it, proving under 5 s on a laptop and under 60 s on a phone, and no cryptographic component without a firm's review.

06 / Next

Read the circuit, then run one

docs/CIRCUIT.md is the constraint-by-constraint account, and section 10.7 is the disclosure list above in its original form. docs/DESIGN.md carries the chain, the emission and the open questions. docs/BENCH.md carries every figure on this page and the machine it came from.

None of it asks to be believed. Clone the repository, build it, start a devnet, and check the numbers against your own machine. Mine it or read what Qloak can and cannot do.