Shielded ETH Pool
Core on-chain model for native ETH shielded notes, Merkle commitments, and Groth16 spends on Base.
This page is adapted from the internal protocol spec (dome/docs/protocol/shielded-eth-base.md). It describes the cryptographic and on-chain model for Dome on Base.
Live testnet today: Base Sepolia uses the EtherPool proxy with a unified transact() entrypoint, NewCommitment events, and optional @dome/backend indexer + relayer. The RPC-only sync path below still applies if you scan events directly — the indexer is a convenience layer, not a trust requirement for reads.
Threat model
The protocol reduces public address linkability between deposits, private transfers, and withdrawals. It does not hide:
- Network metadata (RPC provider, IP, timing)
- The EVM account that pays gas on self-submitted withdraws
- Value/timing correlation when the anonymity set is small
Users should withdraw to fresh addresses and fund gas in a privacy-conscious way. Relayers (optional on testnet) submit withdraws so recipients do not need prefunded gas keys linked to the deposit path.
Chain and asset
| Chain | Base (Sepolia for development, Base mainnet post-audit) |
| Asset | Native ETH only (msg.value deposits, contract balance vault) |
| Amounts | uint256 wei in notes and proofs (field-safe encoding in circuits) |
State model
The EtherPool contract holds pooled ETH and on-chain state:
- Sparse Poseidon Merkle tree (depth set at deploy — 26 levels on current testnet, ~67M leaves)
- Incremental root updates via
filledSubtrees - Nullifier set to prevent double-spends
- Pausing / upgradeability via proxy (testnet deployments)
Clients can rebuild commitment and nullifier history by scanning contract events with eth_getLogs in bounded block ranges, caching progress locally. The Dome indexer performs the same scan server-side for wallets that opt in.
Note format
Private notes are never stored on-chain in plaintext.
note = {
asset_id: bytes32, // ETH asset identifier (field-encoded)
amount_wei: u256,
owner_spend_pubkey: bytes32,
owner_view_pubkey: bytes32,
rho: bytes32,
rseed: bytes32,
}Derived values (Poseidon on BN254):
commitment = H(asset_id, amount_wei, owner_spend_pubkey, rho, rseed)
nullifier = H(owner_spend_secret, rho)
note_tag = H(owner_view_pubkey, rho)Contract API (current)
Production testnet pools expose a single transact(Proof, ExtData) function on EtherPool (and ERCPool for ERC-20 in future). Deposits and withdrawals are both transact calls with different proof public inputs.
Typical flow:
- Deposit — Client generates a Groth16 proof, sends
transactwithmsg.valuematching the note amount. Pool appends the output commitment and emitsNewCommitment. - Withdraw — Client proves spend of existing notes; pool checks nullifier unused, verifies proof, transfers ETH to recipient (via relayer or self-submit).
Public signals (withdraw)
Public signals follow the circuit layout (6 signals):
- Merkle root
- Nullifier
- Output commitment A (empty sentinel if none)
- Output commitment B (empty sentinel if none)
- Withdraw wei
- Relayer fee wei (0 when self-submitting)
See Deposit & Withdraw for the wallet/SDK flow end-to-end.
Events (canonical sync source)
Current EtherPool events (simplified):
event NewCommitment(bytes32 commitment, uint256 index, bytes encryptedOutput);
// Nullifier consumption is enforced in-contract; scan transact() logs + pool stateHistorical RPC-only spec used CommitmentInserted / NullifierSpent naming — same idea: events + eth_getLogs are the portable sync source.
RPC-only recovery
- Configure a public Base RPC URL, pool contract address, and deployment block.
- Scan commitment (and spend) events from deployment block to latest, in chunks (e.g. 2,000–10,000 blocks per
eth_getLogsrequest). - Persist
lastScannedBlockin local encrypted storage. - Decrypt
encryptedOutputpayloads with the view key, filter spent nullifiers, rebuild the Merkle tree, compute shielded balance.
No Dome HTTP API is required for reads — see Backend & Indexer for the hosted shortcut.
Privacy requirements
- Wallets should generate fresh withdrawal addresses by default.
- No app analytics with addresses, notes, nullifiers, or proof inputs.
- UI should communicate anonymity set size and timing risks.
- Users may choose their own public RPC endpoint.
Development and security
- Pre-production: dev verifying keys, unaudited contracts/circuits on testnet.
- Production requires: audited contracts, audited circuits, trusted setup, production verifier on-chain, no dev proof bypasses.
For local deployment and Hardhat workflow, see the monorepo dev scripts (bash scripts/dev/up.sh) or Architecture.