ROOTVIELROOTVIEL
← Academy

Module 14 · Privacy protocols in practice: shielded pools, Railgun & the compliance frontier

Railgun: shielded balances as a smart contract

Railgun takes the deposit/withdraw privacy-pool pattern and generalises it into a shielded balance system for arbitrary ERC-20 tokens, implemented as a smart contract rather than a purpose-built chain. Value enters the shielded set by a shield (public → private), moves between 0zk shielded addresses by private transfers that never touch the public ledger, and leaves by an unshield (private → public). Internally it is the Zcash note model ported to an account-ecosystem chain: each private balance is a set of commitment notes, the contract holds a commitment accumulator (a Merkle tree) and a spent-nullifier set, and every private action is a zero-knowledge proof that spends old notes (publishing their nullifiers) and creates new ones (publishing their commitments). Viewing keys let a holder selectively disclose their history to an auditor or counterparty — a real, legitimate feature — but the same key de-anonymises every past and future transaction of that account if it leaks, so viewing-key custody is a permanent key-management risk, not a transient one. Relayers (broadcasters) submit the transaction and pay gas so the user's own externally-owned account and ETH balance do not link them; the relayer still learns the recipient and amount of what it broadcasts, and privacy still depends on the size of the anonymity set.

Learning objective

Understand how Railgun brings a Zcash-style shielded pool to an account-based, smart-contract chain: how a 0zk shielded address receives value, how shield/unshield cross the public↔private boundary, how the commitment + nullifier accumulator lives entirely in contract state, how private transfers move balances inside the shielded set, how viewing keys allow selective, auditable disclosure (and why a leaked viewing key is a permanent, retroactive catastrophe), and how relayers/broadcasters submit the transaction so the user's own address and gas do not link them — including precisely what the relayer still sees. The point is to generalise the note model to arbitrary ERC-20 balances without overstating what the system hides.

Track
Research
Module
14 · Privacy protocols in practice: shielded pools, Railgun & the compliance frontier
Prerequisites
Nullifiers, Merkle membership and the privacy-pool pattern · Commitment schemes in depth: Pedersen, KZG and IPA · On-chain cryptography and zero-knowledge verification

From a dedicated chain to a shielded contract

Zcash built a shielded pool as a whole chain: a UTXO ledger where private value is held as "notes" (commitments to an amount and an owner) and spent by publishing a nullifier and a zero-knowledge proof. Railgun asks a different question — can that same note model live inside a single smart contract on an account-based chain, for any ERC-20 token, without changing the base layer? The answer is yes, and the price is that the shielded pool is now a contract whose state anyone can read, so every guarantee must come from the cryptography rather than from a private ledger.

The result is a shielded balance system. A user shields tokens into the pool, holds and moves them privately between shielded addresses, and later unshields back to an ordinary public address. Between shield and unshield, the public chain sees only the contract's accumulator growing and nullifiers being published — not who paid whom, nor how much, nor which token. This lesson reuses the privacy-pool pattern you already know (commitment, Merkle membership, nullifier) and shows what changes when the "pool" must support arbitrary balances, internal transfers, selective disclosure, and third-party submission.

Note

Railgun is a real, public protocol; this lesson describes only its well-documented, high-level mechanics and uses synthetic, illustrative code. It does not reproduce exact function names, contract addresses, circuit layouts or figures, and it is not a substitute for the protocol's own specification and audits.

The note model, generalised to ERC-20 balances

In the privacy-pool lesson every deposit was a single fixed denomination, so a commitment only had to bind a secret and a nullifier seed. A shielded balance system must bind more: a note commits to a token type, an amount, and an owning shielded address, as well as the randomness that hides it. A user's private balance in a given token is then simply the set of their unspent notes for that token — exactly the UTXO idea, but held as commitments inside one contract and spendable across many different ERC-20s.

Because amounts are arbitrary rather than a single denomination, spending works like making change: a private action consumes one or more input notes (proving each is a real unspent leaf and publishing its nullifier) and produces output notes (new commitments), under a circuit constraint that inputs and outputs balance for each token. This is the join/split structure of a shielded UTXO system, now expressed as contract state transitions.

0zk shielded address
A Railgun receiving address (its human-readable form is conventionally prefixed "0zk"). It is NOT an on-chain account that holds funds — it is a destination inside the shielded set, bound into note commitments so that only the holder of the corresponding spending key can spend notes sent to it. Sending to a 0zk address creates private notes; it does not emit a public transfer to that address.
Viewing key
A key, separate from the spending key, that lets its holder decrypt and read an account's notes and transaction history without being able to spend. It exists so a holder can selectively disclose their activity — to an auditor, a tax authority, or a counterparty — and prove what they did. Possession of the viewing key reveals history; it does not grant the ability to move funds.
Broadcaster / relayer
A third party that receives a signed, proven private transaction off-chain and submits it on-chain, paying the gas (typically compensated out of the shielded amount). Because the relayer's externally-owned account sends the transaction, the user's own public address and ETH balance are never linked to the shielded action. The relayer learns what it broadcasts — notably recipient and amount — but not the sender's identity.
Nullifier
A value derived deterministically from a note (and the spending key) that is published when the note is spent and recorded in the contract's spent set. It makes each note spendable exactly once without revealing which commitment it corresponds to. As in the privacy-pool pattern, uniqueness is enforced on the nullifier, never on the proof bytes.

Shield and unshield: the public↔private boundary

There are exactly two points where value is visible: entering and leaving the shielded set. A shield transfers tokens from a public address into the contract and mints equivalent private notes to a 0zk address — this step is public, because an ordinary ERC-20 transfer into the contract is on the chain for all to see. An unshield spends private notes and releases public tokens to an ordinary address — also public, for the same reason. Everything strictly between those two events happens as internal note transitions and reveals no sender, receiver, amount or token on the public ledger.

This boundary is where linkage risk concentrates. If a user shields a distinctive amount and shortly after someone unshields the same distinctive amount, an observer can correlate the two across the boundary even though the contract revealed nothing about the private hops in between. Crossing the boundary is where anonymity-set reasoning (from the privacy-pool lesson) applies most sharply.

Shield → private transfer → unshield, as contract state

The accumulator and the spent-nullifier set are plain contract state. A private action is a zero-knowledge proof whose public inputs include the accumulator root it was built against, the nullifiers of the notes it spends, and the commitments of the notes it creates; its private witness is the notes, spending key and Merkle paths. The contract checks the root is known, that no published nullifier is already spent, that the proof verifies, then records the nullifiers and inserts the new commitments.

synthetic — shielded-balance lifecycle on an account-based chain (illustrative)
# ---- SHIELD (public -> private): visible on-chain ---- # user sends 100 TOKEN from their public address into the pool contract # contract mints a private note to a 0zk destination note_in = Note(token=TOKEN, amount=100, owner=zk_addr_A, rand=random()) C_in = Commit(note_in) # token + amount + owner + randomness tree.insert(C_in) # accumulator (Merkle tree) grows # ---- PRIVATE TRANSFER (private -> private): nothing public but proof+state # A pays 30 TOKEN to B, keeps 70 as change — a join/split, like making change # circuit (private: notes, spend_key, paths; public: root, nullifiers, commitments) assert spent_note.owner == derive(spend_key) # A owns the input assert merkle_root(Commit(spent_note), path) == root # membership assert N == Nullifier(spent_note, spend_key) # nullifier binding assert sum(inputs.amount) == sum(outputs.amount) # value conserved, per token C_B = Commit(Note(TOKEN, 30, zk_addr_B, random())) # payee note C_chg = Commit(Note(TOKEN, 70, zk_addr_A, random())) # change note back to A # ---- contract state transition (Solidity-shaped) ---- function transact(bytes proof, uint root, uint[] N, uint[] C) external { require(knownRoots[root], "unknown or stale root"); for (i in N) require(!spent[N[i]], "nullifier already used"); require(verifier.verify(root, N, C, proof), "bad proof"); for (i in N) spent[N[i]] = true; # mark spent BEFORE inserting outputs for (j in C) tree.insert(C[j]); # new commitments enter the accumulator } # ---- UNSHIELD (private -> public): visible on-chain ---- # B spends its note, contract releases 30 TOKEN to a public address # proof publishes B's nullifier; contract pays out the public amount

Nothing in the private-transfer step names A, B, the amount or the token on the public ledger: the chain sees a proof, some nullifiers, and some new commitments. The contract is a shielded pool with internal transfers and change-making — the Zcash note model, but as account-chain contract state over arbitrary ERC-20s rather than a dedicated UTXO ledger.

Viewing keys and relayers: what they give and what they cost

Two features make a shielded contract usable in practice, and each carries an honest limit. Viewing keys provide selective, auditable disclosure: a holder can hand an auditor a key that decrypts their note history, proving what they transacted without surrendering the ability to spend. This is genuinely valuable — it lets a private account be audited or reconciled on demand. But the viewing key is a read-capability over the entire account, past and future, so disclosure is all-or-nothing for whatever the key covers, and leakage is permanent.

Relayers (broadcasters) solve a subtler leak. A user who submitted their own shielded transaction would pay gas from their own externally-owned account, and that public account — its address, its ETH balance, its history — would link them to the action. A relayer submits and pays instead, breaking that link. In exchange the relayer sees the transaction it broadcasts: for an unshield or a payment it broadcasts, it learns the recipient and the amount. It does not learn who the sender was, but it is not a blind party.

synthetic — selective disclosure via viewing key, and its blast radius (illustrative)
# Selective disclosure: hand an auditor the viewing key (NOT the spend key) history = decrypt_notes(account_notes, viewing_key) # read-only over the account # auditor can now reconstruct every note this account ever sent/received, # prove balances, reconcile flows — WITHOUT being able to spend anything. # Blast radius if the viewing key leaks: # - it decrypts ALL notes the account has ever had, retroactively, and # - it will decrypt every FUTURE note under the same key. # => a single leak de-anonymises the whole account's history, permanently. # Rotating a spend key does not undo a viewing key already disclosed.
Note

Honest limits. (1) A relayer is not blind: it learns the recipient and amount of the transaction it broadcasts — it just does not learn the sender's public identity. (2) Viewing-key leakage is retroactive and permanent: whoever holds the key can decrypt the account's entire history and all future activity under it, and there is no revocation that un-reveals already-decryptable notes — so viewing keys are a lasting key-management liability, not a transient one. (3) Shielding is not a magic cloak: privacy still depends on the anonymity set. Distinctive amounts, tight timing across the shield/unshield boundary, reusing the same relayer, or a thin pool can narrow the set toward one regardless of the proof.

Reading the guarantee honestly (Rootchain)

When a Railgun-style transaction verifies, the contract has established a precise statement: the caller knows the spending keys for some unspent notes under a valid accumulator root, the published nullifiers are bound to those notes, the output commitments are well-formed, and value is conserved per token — all under the circuit's constraints, the commitment scheme's hardness assumptions, and correct proof verification. That is what prevents theft, forgery and double-spends inside the shielded set, and no more.

It does not establish that the sender is anonymous (the anonymity set is a separate, measured quantity, and it collapses under distinctive amounts, tight timing or a thin pool), that the counterparties were entitled to the funds, that the relayer learned nothing (it learned recipient and amount), or that no viewing key has been disclosed (a leaked viewing key retroactively exposes the whole account). For Rootchain this is a textbook evidence-model case: a valid proof attests to the spend/conservation statement under stated assumptions; it is PROVEN that balances cannot be forged or double-spent, NOT PROVEN that any given user is actually unlinkable, and UNKNOWN whether a given account's viewing key has leaked. Report the proven statement with its assumptions, report the anonymity set as its own measured figure, and treat viewing-key custody as an open, permanent risk rather than a solved one. Rootzk is the ROOTVIEL module where these privacy guarantees are studied; it is not this protocol.

Exercise

Note

A team deploys a Railgun-style shielded ERC-20 pool. In review, three claims are made: (a) "Because transfers happen inside the shielded set, the relayer that broadcasts an unshield learns nothing about it." (b) "We give auditors a viewing key for read-only access, so there is no custody risk — they cannot spend." (c) "The ZK proof guarantees our users are anonymous." For each claim, say whether it is correct, partially correct, or wrong, and why. Then state which single operational mistake most easily links a user across the public↔private boundary, and the fix.

Expected direction: Claim (a) is WRONG as stated: a relayer that broadcasts an unshield (or a payment) learns the recipient and the amount of that transaction — it just does not learn the sender's public identity. It is not a blind party. Claim (b) is PARTIALLY correct: a viewing key is indeed read-only and cannot spend, so handing one out does not risk theft. But it is wrong that there is "no custody risk" — the viewing key decrypts the account's entire past and future history, retroactively and permanently, so if it leaks the whole account is de-anonymised and there is no revocation that un-reveals it. Viewing-key custody is a lasting key-management liability. Claim (c) is WRONG / overstated: the proof guarantees that balances cannot be forged or double-spent, not that any user is anonymous. Anonymity is a separate, measured property of the anonymity set (pool size and behaviour), and it can collapse to one. The single most linking operational mistake is crossing the shield/unshield boundary with a distinctive amount and/or tight timing — e.g. shielding 12.37 TOKEN and unshielding 12.37 TOKEN minutes later — which lets an observer correlate entry and exit regardless of the private hops between. The fix is operational: use common amounts / split across notes, wait across a large and active pool before unshielding, avoid reusing the same relayer, and do not reuse the public addresses on either side of the boundary.

Check your understanding

How does Railgun generalise the Zcash note model, and where do the commitment accumulator and nullifier set actually live?
It ports the shielded-UTXO note model — private balances held as commitment notes, spent by publishing nullifiers and a ZK proof — onto an account-based chain and extends notes to bind a token type and amount, so it works for arbitrary ERC-20 balances rather than a single fixed denomination. The commitment accumulator (a Merkle tree) and the spent-nullifier set live entirely in smart-contract state, which anyone can read, so every privacy guarantee must come from the cryptography, not from a private ledger.
What does a relayer/broadcaster hide, and what does it still see?
It hides the link between the user's own public account and the shielded action: because the relayer submits the transaction and pays gas, the user's externally-owned address and ETH balance are never tied to it. It still sees the transaction it broadcasts — in particular the recipient and the amount of an unshield or payment. It is not blind; it simply does not learn the sender's public identity.
Why is a leaked viewing key a permanent, retroactive problem, and how does it differ from the spending key?
The viewing key is a read capability over the whole account: it decrypts every note the account has ever held and every future note under the same key. So a single leak de-anonymises the account's entire history retroactively and continues to expose future activity, with no revocation that un-reveals already-decryptable notes. It differs from the spending key in that it cannot move funds — disclosing it risks confidentiality, not theft — but that also means rotating the spending key does nothing to repair a disclosed viewing key.
If the proof is sound, can a Railgun-style user still be de-anonymised, and how?
Yes. The proof only guarantees that balances cannot be forged or double-spent; it says nothing about linkability. A user is de-anonymised when the anonymity set collapses — most easily by correlating the public shield and unshield events across the boundary using distinctive amounts, tight timing, address reuse, or always using the same relayer, or simply by transacting in a thin pool. Anonymity is a measured property of pool size and behaviour, separate from proof soundness.

References

  1. [1]Banco ZK — Vol. II/III (2025): shielded pools, note model, nullifiers, viewing keys and relayer-based submission — external educational reference
  2. [2]Hopwood, Bowe, Hornby & Wilcox — Zcash Protocol Specification (the shielded-note / commitment / nullifier model that Railgun generalises)
  3. [3]Railgun protocol public documentation and audited contracts (high-level shielded-balance, 0zk-address, viewing-key and broadcaster mechanics), as publicly documented

References point to public, well-known technical sources for further study.

Related articles