Architecture
Technical overview of RAIL20's per-token privacy pools, ZK circuit, and data flow on Base, Robinhood Chain, Arbitrum, and BNB Smart Chain.
System Overview
RAIL20 is a zero-knowledge privacy pool system live on Base (chain 8453), Robinhood Chain (chain 4663), Arbitrum (chain 42161), and BNB Smart Chain (chain 56). There is one independent ZK pool contract per token per chain - not a single shared vault. Value lives inside a pool as private UTXO notes; nothing on-chain reveals amounts or recipients.
- Privacy Pools - one per token per chain (ETH + USDC on Base, ETH + USDG on Robinhood, ETH + USDC on Arbitrum, BNB + USDC + USDT on BSC). Each holds shielded balances, maintains a Poseidon commitment Merkle tree, verifies zk-SNARK proofs, and records spent nullifiers.
- Circuit + Verifier - a single UTXO circuit proves note ownership, value conservation, and correct new commitments. Verification runs on-chain per spend.
- Relayer (off-chain) - broadcasts users' proofs so users pay no gas and their wallet never touches the transaction. Charges a flat fee plus a percentage.
- Backend API -
api.rail20.orgserves balance, deposit, withdraw, bridge, swap, and intent-status data to the app.
Data Flow
The client signs a message to derive its account keys, then calls the relayer. The relayer scans the pool for the user's notes, builds the zk-SNARK proof, and broadcasts the transaction. Proof generation happens on the relayer, not in the browser.
User (browser)
|
+-- 1. Sign the RAIL20 message (derives account keys locally)
+-- 2. POST { signature, pool, amount, recipient } to the relayer
v
Relayer (api.rail20.org)
|
+-- 3. Derive keys from the signature; scan the pool for the user's notes
+-- 4. Build the zk-SNARK proof (2 inputs, 2 outputs)
+-- 5. Submit the transact() call (relayer pays gas, takes flat + 0.35% fee)
v
Privacy Pool (on-chain, per token)
|
+-- 6. Verify the proof against a recent Merkle root
+-- 7. Reject if any input nullifier is already spent
+-- 8. Record the nullifiers as spent
+-- 9. Insert the new output commitments into the tree
+-- 10. Settle any public withdraw amount to the recipient
v
ZK Circuit Design
RAIL20 uses a fixed 2-input / 2-output JoinSplit circuit (in the style of Light Protocol's UTXO design). Each spend consumes up to 2 input notes and creates up to 2 output notes. The circuit proves, in zero knowledge: each input note exists in the Merkle tree, each input nullifier is correctly derived, the sum of inputs equals the sum of outputs plus the public amount (deposit/withdraw/fee), and each output commitment is well-formed. All notes in one transaction share a single mintAddress (the token), so a pool only ever mixes its own asset.
| Parameter | Value |
|---|---|
| Proof system | Groth16 (zk-SNARK) |
| Circuit | Transaction(26, 2, 2) - JoinSplit, 2 in / 2 out |
| Hash function | Poseidon (poseidon-lite / circomlib) |
| Curve | BN254 (alt_bn128) |
| Merkle tree depth | 26 levels (up to 33,554,432 leaves) |
| Note encryption | AES-256-GCM (key derived from wallet signature) |
| Double-spend guard | Nullifier set (append-only) |
The circuit's public signals are: root (the Merkle root proven against), publicAmount (external amount minus fee - positive on deposit, negative on withdraw, zero on an internal transfer), extDataHash (binds recipient, relayer, and fee so they cannot be tampered with), the input nullifiers, and the output commitments.
Merkle Tree
Each pool maintains an append-only Poseidon Merkle tree of note commitments. Spending a note reveals its nullifier - not its leaf - so the tree grows monotonically while spent notes are tracked separately by the nullifier set.
commitment = Poseidon(amount, pubkey, blinding, mintAddress)
nullifier = Poseidon(commitment, merklePath, signature)
// signature = Poseidon(privKey, commitment, merklePath)
pathElements[] // sibling hashes for Merkle inclusion
pathIndices[] // 0=left, 1=rightSecurity Model
- zk-SNARK soundness - over BN254, no valid proof exists without knowledge of the private inputs, so notes cannot be forged.
- Poseidon collision resistance - commitments and nullifiers are computed with Poseidon; no known collisions.
- Append-only nullifier set - prevents double-spend without revealing which note was spent.
- Encrypted notes - each note's data is stored as an AES-256-GCM ciphertext on-chain, encrypted under a key derived from the owner's wallet signature. Only the owner can decrypt and recover their balance; no server holds it.
- Amount & recipient privacy - on-chain data is limited to commitments and nullifiers; amounts and recipients are never revealed.