Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

How It Works

Pampalo is a ZK private-money protocol. Value lives in notes committed to an on-chain Merkle tree, spent with zero-knowledge proofs, and addressed by an unlinkable Poseidon identifier rather than an Ethereum address. The nine figures below walk the protocol end to end, from a single balanced transfer up to the full shield → transfer → unshield journey.

Sum in = sum out

No balance is edited. Alice destroys her 5 USDC note and the transfer creates two fresh notes: 2 to Bob, 3 back to herself as change. The chain never sees the amounts, yet the proof guarantees inputs and outputs balance for every asset.

Inputs · destroyedTransferOutputs · createdNote A5USDCowner · AlicespentTransferspend ▸ createNote B2USDC→ BobNote C · change3USDC→ Alice52 + 3Σ inputs = Σ outputs · per asset

Anatomy of a note

A note is four fields hashed by poseidon2 into one leaf, the only thing that goes on-chain. The secret is the spend key and is never published. And owner isn't an Ethereum address: one recovery phrase derives three identities, and the note's owner is the Poseidon identifier.

Notefour-tupleasset_idERC-20 / ETHhiddenasset_amount≤ 128 bitshiddenownerPoseidon identifierhiddensecretnever on-chain · spend keyposeidon2SNARK-friendly hashleaf · commitment0x9f3a…b27e→ inserted into the Merkle treeOne recovery phrase → three identitiesrecoveryphrasePoseidon identifier= note ownerEVM addresspublic · gasEnvelope keysecp256k1

The note tree

Each note is a leaf. To prove your note is real you reveal a sibling hash at each level; re-hashing up the path must reproduce a known root, without disclosing which leaf you are. A 2048-leaf tree is addressed by (tree_index, leaf_index); when it fills, the epoch rolls over and the old root stays valid forever.

on the membership pathsibling hash (in the proof)empty slot · ZERO_LEAFrootleavesroot32-byte fingerprint · valid foreversiblevels 2–10 omittedmy noteZERO_LEAFRe-hash my note up the path → reproduces a known root. Proves membership, reveals nothing.Tree rotation · epochs (append-only, old roots never expire)Tree 0full · root frozen ✓Tree 1filling · active(tree 1, leaf 37)Tree 2empty · nextIndex 0fullfull

Spend = nullify

The leaf is never removed; the tree is append-only. Spending publishes a nullifier, a deterministic fingerprint that drops into the nullifierUsed set. Spend the same note again and it yields the same nullifier, which is already present, so the second attempt bounces. Spentness lives in a separate set, unlinkable to the leaf.

note leaf0x9f3a…stays in tree · untouchedspendnullifier0x4c…e9spend againsame nullifier0x4c…e9already spentnullifierUsedspent fingerprints · a separate set0x71…a20x4c…e9new0xb3…07nullifier = poseidon2( leaf_index, owner, secret, asset_id, asset_amount )

The proof as a sealed box

Private witnesses enter the sealed side and never leave the client. The proof checks five things (owned, in-tree, balanced, nullifier-correct, outputs-committed) and emits only the public inputs the chain needs: root, nullifiers, output_hashes. Bounding every amount to 128 bits makes the p − N overflow unrepresentable, so value can't be conjured from nothing.

Private witnesses · never leave the clientinput notessecretsMerkle pathsoutput notesTransfer proofchecks, then reveals nothing elseowned · poseidon2(owner_secret)=ownerin-tree · leaf re-hashes to rootbalanced · Σin = Σout, per assetnullifier · matches the noteoutputs committed · = output_hashesPublic inputs · all the chain seesrootnullifiers[ ]output_hashes[ ]Field-overflow guardamounts bounded to 128 bits, blocking the p − N mint trick02¹²⁸p − N

Sending a note's secret

The note's creator generates the secret and ECIES-encrypts it to the recipient's Envelope key. Only the ciphertext rides on-chain with the leaf, so the server and relayer never see the plaintext. The recipient's wallet scans events and decrypts with its Envelope private key, then files the note in local storage. A wallet actually holds two envelope keys — a shared one and an isolated one for mainnets — and scanning trial-decrypts against both, so a note arrives no matter which the sender used. (SDK docs.)

Sender · creatorOn-chain · publicRecipient · walletnew note0x9f3a…secretECIES encryptto recipient'sEnvelope key (public)→ 0x7c1f…adleaf insertedNotePayloadencryptedPayloadserver / relayer reads0x7c1f…ad ✓ ciphertextscan eventsECIES decryptwith Envelopeprivate key→ secret revealedfile note locallyIndexedDB

The contracts on Ethereum

The Pampalo contract is the hub: every entry point routes its proof to one of four verifiers before mutating the two pieces of state: the Merkle tree and the nullifier set. Shielding isn't instant: a deposit is escrowed in a Queued state for a one-hour compliance wait. After it, anyone can executeShield; before it, the shielder can cancel, a vigilant citizen can contest (refunding inline), and a booth operator can bypass the wait.

a · Contract topology
shieldpub→privtransferpriv→privunshieldpriv→pubunshield bundledPampalo.solextends PoseidonMerkleTree · AccessControlstate that mutatesMerkle treeleaves · roots · epochsnullifier setspent fingerprints+ shield queue · USD caps · roles · kill switchverify(proof, publicInputs)Deposit Verifier(shield)Transfer Verifier(transfer)Withdraw Verifier(unshield)TransferExternal Verifier(bundled)
b · Shield-queue state machine
shield()QueuedPendingShield · escrowedunlockTime = now + 1hwait 1hexecuteShield · anyoneExecutedleaf inserted ✓executeShieldImmediate · BOOTH_OPERATOR (bypass wait)CancelledcancelShield · shielderContestedcontestShield · VIGILANT_CITIZENrefund escrow + cap

The relayer breaks the gas link

Broadcasting your own transfer puts your EVM address on the event, linking you to the payment. Instead, you build and prove the transfer client-side and hand the proof and payload to a pool of five interchangeable relayer EOAs (picked LRU). The relayer's address lands on-chain; yours never does. Because transfer is permissionless, the relayer holds no privilege: it can't read your notes or alter the transfer, it only pays gas.

(a) Self-broadcastyou pay your own gasyour walletEVM 0x9a3…f1broadcasts transfer()Contract · transfer eventfrom = 0x9a3…f1public · anyone can read itlinked!your address ⟷ the payment(b) Relayer · gas sponsoryour address stays off-chainclient-side · privateYou · walletproof + payloadEVM 0x9a3…f1stays hereRelayer pool5 EOAs · LRU pick · only pays gasLRUbroadcasts transfer()Contract · transfer eventfrom = 0xRel…7cthe relayer's address, not yoursneverrelayer can't read notes · can't alter the transfer

The full journey

The whole arc in three lanes. Alice shields public USDC into Note A; her wallet decrypts and files it. She transfers 2 to Bob plus 3 change, proving it client-side and broadcasting via a relayer; the contract nullifies A and inserts B and C, and Bob's wallet decrypts his note. Later Bob unshields to a public address. The chain only ever saw balanced proofs and opaque commitments, never the amounts or who paid whom.

CLIENTwalletPAMPALOcontract · treePUBLICchain1 · Shield public → private2 · Transfer private → private3 · Unshield private → publicapprove + shield USDCescrowed · monthly cap1h wait → executeShieldleaf inserted · Note A · 5 USDCdecrypt & file Note Astored locally · IndexedDBbuild B (2→Bob) + C (3)prove transfer · encrypt secretsverify · nullify A · insert B,Cemit NotePayload for eachvia relayerBob decrypts B ✓Bob proves withdrawspend Note Bverify · pay outexit address front-run safeUSDC → any EVM addressmonthly cap appliesNo server ever sees a plaintext note, a secret, or the link between Alice's and Bob's addresses.

Pampalo's private-money core builds on commbank.eth's Private Unstoppable Money protocol and account model.