The sender isa proof, notan address.
Sanect’s shielded pool is a Zcash-Sapling / Penumbra-style note pool inside a single EVM contract. It uses a custom Noir circuit proven with UltraHonk over Barretenberg’s universal SRS — so there is no per-circuit trusted setup — and keys derived from the wallet you already have.
Four ideas behind the pool.
The design is deliberately conventional where it can be — the same note model Zcash has run since 2018 — and specific only where the EVM and wallet UX demand it.
Notes, not accounts
A note is a UTXO: {asset, value, owner_pk, salt}. The chain stores only its Poseidon-2 commitment and, once spent, its nullifier. Balances are reconstructed locally by the owner’s wallet.
Auto-receive, no claim step
Every commitment ships with a 240-byte encrypted memo. Wallets trial-decrypt new memos with the viewing key; a memo that opens is a note you own. Offline for a year — funds are simply there when you return.
Spending is a zero-knowledge proof
To spend, the wallet proves inclusion in the tree, ownership of the spending key, a correctly derived nullifier and value conservation. The chain verifies the proof; nothing else is revealed.
Keys derived from your existing wallet
One canonical message signed once in MetaMask. HKDF-SHA256 turns the signature into a viewing key and a spending key. No new seed phrase; recovery is “connect, sign once.”
What the proof has to satisfy.
A sketch of the constraint set in privacy/circuits/transfer/src/main.nr. Every line is a hard constraint: if any fails, no proof exists.
// for each of the 2 input notes assert(owner_pk == derive_pk(spending_key)); assert(merkle_root(commit(note), path) == root); // inclusion assert(nullifier == hash(note, spending_key)); assert(note.asset_id == asset_id); // for each of the 2 output notes assert(out_commitment == commit(out_note)); assert(out_note.asset_id == asset_id); // conservation — public_amount is +shield / 0 / −unshield assert(in[0].value + in[1].value + public_amount == out[0].value + out[1].value); // public inputs (8): root, 2 nullifiers, 2 commitments, // public_amount, asset_id, recipient (unshield target)
- 1Inclusion, not identity. The proof shows some note in the tree is being spent — not which one. The Merkle path is a private witness.
- 2Nullifiers prevent double-spend. Deterministic per note and only computable with the spending key. Recorded on chain; a repeat reverts.
- 3One circuit, every asset. Notes carry an asset_id; native SNCT and ERC-20s share the same proof system. v1 exposes SNCT.
- 4Larger transfers batch. 2-in/2-out per proof; the wallet chains proofs for more inputs. Gas is public anyway, so batching leaks nothing about who is transacting.
- 5Proving stays on your device. Noir’s wasm prover runs in the browser; the witness — your notes and keys — never leaves it.
One entry point on chain.
ShieldedPool.transact(proof, inputs, memos) handles all three operations. The sign of public_amount decides which.
Shield
Value moves from your public balance into a new note. Observers see a deposit and a commitment — not who will be able to spend it.
Internal send
Two notes in, two notes out, no value leaves the pool. Observers see a nullifier consumed and a commitment added.
Unshield
A note is spent and value is released to an EVM address via an internal call. The recipient is visible; the note’s origin is not.
0x5532Dc2B743D45Fd0121E382696521ED62DD3ecf
0x580d7c1Ff04a5761D58a8Db08C37fb33c5e395f2
0xaEE4A735027b86ccbF43042f4C31d1a856943c1C
Pool owner: protocol-governance 0xd5f05a16329c73efD21a57145A97c2b307E2dFED. Initial root 0x0b59baa3…568d (depth-32 Poseidon-2 zeros).
Keys from the wallet you already have.
The derivation message is frozen. Wallets that derived keys on the first testnet still hold the same keys on mainnet.
- 01personal_sign(DERIVATION_MESSAGE)One signature in MetaMask. Never sent anywhere.
- 02HKDF-SHA256, domain-separatedLabels VIEW_KEY_LABEL / SPEND_KEY_LABEL expand the signature into two independent secrets.
- 03Viewing key → X25519 public keySenders encrypt memos to it with a fresh ephemeral key per memo (ECDH → HKDF → ChaCha20-Poly1305; AAD binds the ephemeral key).
- 04Spending keyPrivate witness in the proof. Derives owner_pk in notes and nullifiers on spend.
What shielding does — and does not — hide.
Privacy that overpromises is worse than none. This is the honest boundary.
- Which note is spent (the input’s position in the tree)
- Who owns a newly created note
- The link between a deposit and a later withdrawal
- Amount and recipient in an internal send
- Anything inside a memo — 99% of memos belong to other people and fail to decrypt
- That the pool was called, when, and how much gas was paid
- Shield amount and unshield amount (value crossing the pool boundary)
- The recipient of an unshield — it lands on a normal EVM address
- The size of the anonymity set. Privacy scales with how many others use the pool; the wallet shows the live number rather than pretending
Planned: an anonymity-maintenance service to keep the set meaningful in quiet periods, and Penumbra-style fuzzy message detection so heavy users can outsource scanning. Both are roadmap items, not shipped features.