03

V1 Mainnet · The note model

The formulas

Menoid keeps no balances. Value lives as notes — like banknotes: each has an owner, a token and an amount, and it is spent whole. The chain never sees a note itself, only numbers derived from it. Here is every formula the protocol uses, what it is for, and a small example of each.

Every Poseidon hash starts with a small number, a domain separator from 1 to 5. It guarantees no hash can ever be mistaken for another kind, even with the same inputs.

TagHashGives you
1Poseidon(1, address, spendPk.x, spendPk.y)userCommitment — your identity
2Poseidon(2, sk)nk — the nullifier key
3Poseidon(3, userCommitment, r)npk — a note’s owner, blinded
4Poseidon(4, token, amount, npk)commitment — the note on-chain
5Poseidon(5, nk, commitment, r)nullifier — the note’s spent marker
ucuserCommitment — your private identity
Formula
userCommitment = Poseidon(1, address, spendPk.x, spendPk.y)
What it is
One number that stands for you inside the pool. It contains your public address and your spending public key.
Why
Because it contains your address, people can pay you at your normal address: the pool maps address → userCommitment. Because it contains spendPk, only the holder of sk can prove it is theirs.
Used in
Registration (stored on-chain), every note you receive (inside npk), and every proof you make — the circuit rebuilds it from sk and your address, and the proof fails if it doesn’t match.

Example

Bob pays Alice. His app reads registrationOf(Alice’s address) and gets uc_A, then locks a note to it. Knowing Alice’s address and uc_A is no help in spending that note: that needs Alice’s sk.
nkThe nullifier key
Formula
nk = Poseidon(2, sk)
What it is
A one-way hash of your spending key.
Why
Nullifiers are computed from nk instead of sk. Poseidon can’t be run backwards, so sharing nk shows which of your notes are spent without giving away sk.
Used in
Every nullifier you publish, and your full viewing key.

Example

An auditor holding Alice’s full viewing key computes the nullifier of each of her notes and sees which were spent, and when. They still can’t make a proof — that needs sk.
npkThe note public key — a note’s owner, blinded
Formula
r   = 31 random bytes   // fresh for every note
npk = Poseidon(3, userCommitment, r)
What it is
The owner of one note, hidden behind that note’s own random number r.
Why
Without r, every note to Alice would carry the same owner value and be trivially linkable. With r, two notes to the same person look unrelated.
Used in
Every commitment — and swaps, where the owner of the bought note (npkOut) is fixed before its amount exists.

Example

Alice gets two payments: Poseidon(3, uc_A, r₁) and Poseidon(3, uc_A, r₂). Two unrelated-looking numbers; nobody can tell they both belong to Alice.
cmcommitment — the note, as the chain stores it
Formula
commitment = Poseidon(4, token, amount, npk)
What it is
A note’s public fingerprint: one leaf of the pool’s Merkle tree. It hides the owner, the token and the amount.
Why
The token sits inside the hash, so one pool and one tree hold every token — and a private send doesn’t even reveal which token moved.
Used in
Created by every mask, send, swap and unmask; proved to exist (with a Merkle path) when the note is spent.

Example

Mask 5 MON: the pool wraps it to WMON and computes Poseidon(4, WMON, 5·10¹⁸, npk) itself, from the tokens it actually received. The 5 can’t be faked — the amount in the note is the amount that arrived.
nfnullifier — the note’s spent marker
Formula
nullifier = Poseidon(5, nk, commitment, r)
What it is
A number published when — and only when — the note is spent.
Why
The pool remembers every nullifier and refuses one it has seen, so no note is spent twice. Only the owner (nk and r) can compute it, and nobody can link it back to its commitment — spending doesn’t reveal which note was spent.
Used in
Every send, swap and unmask publishes exactly 4 nullifiers (empty slots get dummies).

Example

Alice spends her 5 MON note and the proof publishes nullifier N. Anyone submitting N again gets “Nullifier already spent” — and nothing moves.
rootMerkle root — proof that a note exists
Formula
leaf = commitment                   // 0 for an empty leaf
node = Poseidon(left, right)
root = the top of a depth-20 tree   // 1,048,576 leaves; a full tree opens a new one
// every operation inserts one chunk of 4 leaves; the pool keeps the last 100 roots
What it is
One number that summarises every note in a tree.
Why
A proof shows “my note is one of the leaves under this root” without saying which leaf — that is what hides the spender among everyone. Keeping the last 100 roots means a proof made a moment ago still lands after other transactions move the tree.
Used in
Every send, swap and unmask proves its input notes against a recent root.

Example

You prove against root R. Before your transaction lands, three other operations change the tree. R is still one of the last 100 roots, so your proof is accepted.
extextDataHash — everything outside the proof, locked to it
Formula
domain      = keccak256("menoid.v1m.transfer" | "menoid.v1m.withdraw" | "menoid.v1m.swap")
extDataHash = keccak256(abi.encode(chainId, pool, domain, ext)) mod p
What it is
One public input of the proof that commits to the transaction’s extra data: the encrypted notes and the outgoing record — plus the recipient for an unmask, and the adapter, route, minimum output and deadline for a swap.
Why
Change any byte of that data — the recipient, say — and the hash no longer matches the proof, so verification fails. chainId and the pool address stop a proof from being replayed on another chain or pool.
Used in
Send, swap and unmask. p is the BN254 field size.

Example

Alice unmasks to 0xB0B…. If a relayer swapped in its own address, extDataHash would change and the pool would reject the proof. A relayer can carry the transaction or drop it — never redirect it.
encencrypted note — how the owner finds a note
Formula
plaintext     = version ‖ flags ‖ token ‖ amount ‖ r   // 1 + 1 + 20 + 16 + 32 = 70 bytes
encryptedNote = ECIES(owner’s ivkPub, plaintext)       // secp256k1, compressed ephemeral key
What it is
The note’s contents, sealed to its owner’s viewing key and emitted in the NoteCreated event.
Why
Nothing on-chain says which notes are yours. Your app tries to decrypt every note with your ivk; a success that also rebuilds the commitment means the note is yours.
Used in
Every note created. A swap’s bought note carries the flag AMOUNT_FROM_EVENT: its amount is read from the Swapped event, because nobody knew it when the proof was made.

Example

Bob sends Alice 2 USDC. The receiver output is encrypted to Alice’s ivkPub. Her app decrypts it, rebuilds Poseidon(4, USDC, 2·10⁶, Poseidon(3, uc_A, r)), finds it equals the commitment on-chain, and shows +2 USDC.
recoutgoing record — your memo of who you paid
Formula
key    = HKDF-SHA256(ovk, salt = firstNullifier, info = "menoid/v1m/outgoing-record")
record = nonce(12) ‖ AES-256-GCM(key, action ‖ token ‖ amount ‖ fee ‖ counterparty…) ‖ tag(16)
What it is
A private record of each spend, readable only with your ovk: the action (send, unmask, swap), token, amount, fee and counterparty.
Why
Notes you receive can be found with ivk, but a payment you make leaves you nothing to decrypt. The record restores the other side of your history — who you paid — for you and for anyone holding your full viewing key.
Used in
Emitted with every send, swap and unmask as OutgoingRecord(firstNullifier, record).

Example

Alice’s app sees her note’s nullifier N spent, derives the key from her ovk and N, and decrypts: “sent 2 USDC to 0xB0B…, fee 0.01 USDC”.