Reference implementation of EIP-8182 — Native Shielded Pool, tracking the post-pivot Groth16/BN254 + split-proof variant of the spec. Includes circuits, system contract, on-chain verifier wrappers, helper library, a Noir+UltraHonk auth circuit (production-shaped ECDSA-secp256k1 over EIP-712), and the asset-bundle generator the EIP links to.
This is a reference implementation: not audited, not production-ready, and not optimized. Its purpose is to make the protocol concretely buildable, to keep the EIP's normative artifacts (verification key, pool-verify vectors, genesis state dump) in sync with the spec, and to give wallet/protocol developers a complete starting point.
circuits/ Circom (pool + demo auth)
pool/pool.circom fixed-shape pool circuit (Section 8)
auth-demo/auth_demo.circom non-normative demo auth (Groth16, 2 PI)
common/ Poseidon2 BN254 t=4 RF=8 RP=56,
length-tagged sponge, Merkle helpers,
domain-tag constants
circuits-noir/ Noir + UltraHonk (production-shaped auth)
auth/ ECDSA-secp256k1 over EIP-712 typed-data
common/ shared Noir Poseidon2 + helpers
vendor/ pinned keccak256 / poseidon libs
contracts/
src/ShieldedPool.sol system contract (Section 5); inherits
PoolGroth16Verifier so the VK is
embedded in its bytecode (Section 5.5)
src/PoolGroth16Verifier.sol snarkjs-emitted Groth16 verifier;
inherited by ShieldedPool, never
deployed standalone in production
src/AuthDemoGroth16Verifier.sol snarkjs-emitted Groth16 auth verifier
src/auth/DemoAuthVerifier.sol IAuthVerifier wrapping AuthDemoGroth16Verifier
src/auth/HonkRealAuthVerifier.sol bb-emitted UltraHonk Solidity verifier
src/auth/RealAuthVerifier.sol IAuthVerifier wrapping HonkVerifier
script/InstallSystemContracts.s.sol genesis-state installer + dumper
test/Bench.t.sol gas benches (run via `npm run bench`)
test/GasBreakdown.t.sol gas-segmentation smoke test
test/Integration.t.sol mock-verifier integration suite
test/ShieldedPoolAcceptAllHarness.sol test-only: overrides _verifyPoolProof
to no-op for non-verification tests
test/ShieldedPoolStepNineHarness.sol test-only: exposes _verifyPoolProof for
malformed-bytes tests + step-9 bench
test/TransactDemoAuth.t.sol end-to-end with the Groth16 demo auth
test/TransactHonkAuth.t.sol end-to-end with the Noir+Honk auth
src/lib/ JS helper library (Poseidon, intent
formulas, proof byte codec)
scripts/
bench/run.sh + render.js npm run bench harness
circom/ regenerate poseidon2 + domain tags
circuit/build_pool.sh compile pool + run dev trusted setup
integration/build_session.js prove pool + demo auth (Groth16)
integration/build_honk_session.js prove pool + Noir/UltraHonk auth
noir/ Noir witness + EIP-712 typehash gen
witness/ worst-case witness builders
assets/ VK/vector/state-dump generators + refresh.sh
assets/eip-8182/ local copy of the EIP-linked artifacts
build/ circuit + contract build outputs (gitignored)
vendor/circom circom 2.2.3 binary (vendored for repro)
Two groups of files live under assets/eip-8182/:
EIP-linked assets (referenced from EIPS/eip-8182.md; refresh.sh mirrors these to EIP_ASSETS_DIR):
| File | What it pins | Generated by |
|---|---|---|
pool_vk.bin (1728 B) |
Canonical Groth16 verification key (Section 5.5; α∥β∥γ∥δ∥IC[0..19] in EIP-196/197 byte layout) | scripts/assets/vk_to_bin.js |
pool_vk.sha256 |
SHA-256 of the above; the build input the system contract bytecode must have been compiled against | scripts/assets/refresh.sh |
shielded-pool-state.json |
Genesis state dump installed at SHIELDED_POOL_ADDRESS at activation fork (Section 5.1). The dump's bytecode digest is the install-time integrity check (Section 5.5). |
contracts/script/InstallSystemContracts.s.sol |
poseidon2_bn254_t4_rf8_rp56.json |
Round constants + internal diagonal | hand-curated, never regenerated |
poseidon2_vectors.json |
Cross-impl test vectors for the sponge | hand-curated |
Reference-impl test fixtures (used by this repo's tests; not part of the EIP, not mirrored to the spec repo):
| File | What it pins | Generated by |
|---|---|---|
pool_verify_happy_path.json |
Valid (pA, pB, pC, pubSignals) — ShieldedPool.verifyProof MUST return true |
scripts/assets/gen_pool_verify_vectors.js |
pool_verify_invalid_proof.json |
Bit-flipped C.x — verifier MUST return false |
same |
pool_verify_noncanonical_field.json |
publicInputs[0] = p — verifier MUST reject (Section 3.5) | same |
Refresh everything in one command:
EIP_ASSETS_DIR=/path/to/EIPs/assets/eip-8182 ./scripts/assets/refresh.shWithout EIP_ASSETS_DIR the bundle stays in assets/eip-8182/ for local inspection.
- Node 20+
- Foundry (
forge,anvil) - The vendored
vendor/circomworks on macOS arm64; on other targets, install circom 2.2.3 and pointCIRCOMat it - For the Honk auth flow /
npm run bench:nargoand Aztec'sbbonPATH jq(used by the install script and the asset refresh)- ≈700 MB of disk + ≈5 GB of RAM for the snarkjs trusted setup
npm install
./scripts/circuit/build_pool.sh # one-time: compile + dev-MPC trusted setup
forge buildThe included build/pool/pool_final.zkey is a single-contributor dev key. Production deployments MUST replace it with the output of a real multi-party ceremony (see "Trusted setup" below).
node scripts/integration/build_session.js # writes build/integration/session.json
node scripts/integration/build_honk_session.js # optional: enables TransactHonkAuthTest + Honk benches
forge testThe Groth16 demo path needs only the first command. TransactHonkAuthTest and the Honk-side benches require the second; they cleanly skip (forge reports SKIPPED) if the Honk session JSONs are missing, so the Noir+bb toolchain (nargo, bb) is optional for a green forge test.
The five test files exercise:
Integration.t.sol— inline pool-verify happy/sad paths (callingShieldedPool.verifyProofand the step-9 harness),DemoAuthVerifier, fulltransactagainst the accept-all harness, register/deposit/deregister flows.TransactDemoAuth.t.sol— end-to-endtransactwith a real Groth16 pool proof + a real Groth16 demo auth proof.TransactHonkAuth.t.sol— end-to-end with a real Groth16 pool proof + a real Noir/UltraHonk auth proof (the production-shaped ECDSA-secp256k1 circuit).GasBreakdown.t.sol— coarse gas-segmentation smoke test.Bench.t.sol— the seventest_bench_*benchmarks consumed bynpm run bench.
npm run benchBuilds the three transact sessions (transfer + withdraw_eth + withdraw_erc20), runs the seven gas benches in forge, and renders a combined report. Headline numbers below are illustrative (Apple M5 Max, rapidsnark native prover for both Groth16 circuits — set RAPIDSNARK_PROVER=/path/to/prover before npm run bench to enable it; without it, prove times fall back to snarkjs WASM and are roughly 10× slower):
EIP-8182 benchmarks
Pool circuit: Groth16/BN254, rapidsnark (native)
Auth circuit: Groth16/BN254 demo (snarkjs / rapidsnark)
Per-operation summary
─────────────────────
operation pool prove auth prove wallet e2e on-chain gas
set auth policy — — — 2,019,847
deposit (ETH) — — — 1,811,441
deposit (ERC-20) — — — 1,836,602
transfer 0.12 s 0.03 s 0.15 s 3,306,055
withdraw (ETH) 0.12 s 0.03 s 0.15 s 3,340,785
withdraw (ERC-20) 0.12 s 0.03 s 0.15 s 3,332,563
Gas breakdown by operation
══════════════════════════
set auth policy
────────────────────────────────────────────────────────────────────────
Mandatory (everything; setAuthPolicy has no auth choice)
Tx base intrinsic (Berlin) 21,000 1.0 %
Calldata (selector + args) 1,600 0.1 %
EIP-8182 intrinsic (tree write + writes) 1,997,481 98.9 %
Total 2,020,081 100.0 %
deposit (ETH)
────────────────────────────────────────────────────────────────────────
Mandatory (everything; deposit has no auth choice)
Tx base intrinsic (Berlin) 21,000 1.2 %
Calldata (selector + args) 1,396 0.1 %
EIP-8182 intrinsic (body+final hash, tree insert, history, event) 1,789,045 98.8 %
Total 1,811,441 100.0 %
deposit (ERC-20)
────────────────────────────────────────────────────────────────────────
Mandatory (everything; deposit has no auth choice)
Tx base intrinsic (Berlin) 21,000 1.1 %
Calldata (selector + args) 1,624 0.1 %
ERC-20 asset path (4× balanceOf + 1× transferFrom) 29,703 1.6 %
EIP-8182 intrinsic (body+final hash, tree insert, history, event) 1,784,275 97.2 %
Total 1,836,602 100.0 %
transfer
────────────────────────────────────────────────────────────────────────
Mandatory (paid by every user, independent of auth choice)
Tx base intrinsic (Berlin) 21,000 0.6 %
Calldata: pool proof (256 B) 4,048 0.1 %
Calldata: rest (public inputs, ond bytes, selector, offsets) 10,220 0.3 %
Pool proof verify (inline, EIP-8182 §5.5) ~316,000 ~10 %
EIP-8182 intrinsic (tree, hashing, writes, dispatch) 2,746,150 83.1 %
Mandatory subtotal 3,097,211 93.7 %
Discretionary (your auth circuit choice; here: Groth16, 2 PI)
Auth proof verify (Solidity Groth16, 2 public inputs) 204,724 6.2 %
Calldata: auth proof (256 B Groth16) 4,096 0.1 %
Discretionary subtotal 208,820 6.3 %
Total 3,306,031 100.0 %
For comparison — if discretionary uses UltraHonk auth (the Noir+ECDSA
circuit benched here for prove time):
Auth verify (Honk): 2,583,376; proof: 9,792 B (calldata 151,560)
Total with Honk auth 5,832,147
withdraw (ETH)
────────────────────────────────────────────────────────────────────────
Mandatory (paid by every user, independent of auth choice)
Tx base intrinsic (Berlin) 21,000 0.6 %
Calldata: pool proof (256 B) 4,060 0.1 %
Calldata: rest (public inputs, ond bytes, selector, offsets) 10,448 0.3 %
Pool proof verify (inline, EIP-8182 §5.5) ~316,000 ~10 %
Asset movement (ETH/ERC-20 transfer) 34,423 1.0 %
EIP-8182 intrinsic (tree, hashing, writes, dispatch) 2,746,205 82.2 %
Mandatory subtotal 3,131,929 93.7 %
Discretionary (your auth circuit choice; here: Groth16, 2 PI)
Auth proof verify (Solidity Groth16, 2 public inputs) 204,724 6.1 %
Calldata: auth proof (256 B Groth16) 4,096 0.1 %
Discretionary subtotal 208,820 6.3 %
Total 3,340,749 100.0 %
For comparison — if discretionary uses UltraHonk auth (the Noir+ECDSA
circuit benched here for prove time):
Auth verify (Honk): 2,583,376; proof: 9,792 B (calldata 151,476)
Total with Honk auth 5,866,781
withdraw (ERC-20)
────────────────────────────────────────────────────────────────────────
Mandatory (paid by every user, independent of auth choice)
Tx base intrinsic (Berlin) 21,000 0.6 %
Calldata: pool proof (256 B) 4,096 0.1 %
Calldata: rest (public inputs, ond bytes, selector, offsets) 10,712 0.3 %
Pool proof verify (inline, EIP-8182 §5.5) ~316,000 ~10 %
Asset movement (ETH/ERC-20 transfer) 23,629 0.7 %
EIP-8182 intrinsic (tree, hashing, writes, dispatch) 2,748,549 82.5 %
Mandatory subtotal 3,123,779 93.7 %
Discretionary (your auth circuit choice; here: Groth16, 2 PI)
Auth proof verify (Solidity Groth16, 2 public inputs) 204,724 6.1 %
Calldata: auth proof (256 B Groth16) 4,096 0.1 %
Discretionary subtotal 208,820 6.3 %
Total 3,332,599 100.0 %
For comparison — if discretionary uses UltraHonk auth (the Noir+ECDSA
circuit benched here for prove time):
Auth verify (Honk): 2,583,376; proof: 9,792 B (calldata 151,632)
Total with Honk auth 5,858,787
A few things worth knowing about the numbers:
Totalis the full all-in tx gas: 21K base intrinsic + calldata (EIP-2028: 16/non-zero, 4/zero) + EVM execution gas. Not just thegasleft()delta around the pool call.- Pool proof verify is the inline EIP-8182 §5.5 path (≈316K gas at this circuit size): 256-byte calldata decode + 19-field repack + warm self-staticcall to the verifier inherited into the system contract. Measured directly via
ShieldedPoolStepNineHarness.exposeVerifyPoolProof. - Headline gas models a Groth16 auth verifier — same proof system as the pool, 2 public inputs (≈205K verify, 256 B proof). That's what a gas-conscious implementer would deploy. The "For comparison" block under each transact op shows what the Noir/UltraHonk auth circuit costs (≈10× more verify gas, ≈38× more calldata) — that's the circuit the prove timings actually measure.
- Prove timings are single-shot snarkjs WASM for the pool; rapidsnark is ≈10× faster (see "Native pool prover" below).
build/bench/report.json contains the same data in machine-readable form.
Stress-test the pool prove with rapidsnark against build/pool/witness.wtns (built during npm run bench):
/usr/bin/time -p "$RAPIDSNARK_PROVER" \
build/pool/pool_final.zkey \
build/pool/witness.wtns \
pool-proof.rapidsnark.json \
pool-public.rapidsnark.json
npx snarkjs groth16 verify \
build/pool/pool_vkey.json \
pool-public.rapidsnark.json \
pool-proof.rapidsnark.jsonObserved native result on April 29, 2026:
Machine: MacBook Pro, Apple M5 Max, 18 CPU cores, 128 GB RAM, macOS 26.4
Prover: iden3 rapidsnark v0.0.8, macOS arm64 release
real 0.68
user 4.95
sys 0.43
snarkJS: OK!
The pool circuit has 42,959 constraints, 19 public inputs, and 140 private inputs. The benchmark witness is the worst-case private-transfer shape: two real input notes, three real output notes, private fee output active, and all three output slots locked.
Normative (defined by the EIP, copied into assets/eip-8182/ and referenced from the spec):
pool.circomand the four common modules (poseidon2_perm,poseidon2_sponge,merkle,components,bits,domain_tags). The constraint relation MUST match the spec's Section 8 description.pool_vk.binand its sha256 — pinned byEIPS/eip-8182.mdSection 5.5.- The shielded-pool genesis state dump.
Non-normative (the EIP only requires that some contract implementing the interface exists at SHIELDED_POOL_ADDRESS, not this specific Solidity):
ShieldedPool.solitself — any equivalent contract works. The system contract may expose pure helper functions inherited from the embedded verifier (e.g.verifyProof); these are non-state-changing and not part of the normative protocol surface.- The two auth circuits are both examples of Section 11 companion ERCs:
circuits/auth-demo/auth_demo.circom+DemoAuthVerifier.sol— Groth16, 2 public inputs, ≈2,112 R1CS constraints, ≈0.1s prove. Intentionally minimalist; useful for gas modeling and integration testing but does not validate prover feasibility for a real auth circuit.circuits-noir/auth/+HonkRealAuthVerifier.sol+RealAuthVerifier.sol— Noir/UltraHonk, ECDSA-secp256k1 over EIP-712 typed-data, with in-circuit keccak. ≈1s prove on M5 Max. Shows that an end-to-end production-shaped auth path works against the sametransactinterface as the demo. Companion ERCs deploy auth circuits permissionlessly per Section 11.
The included pool_final.zkey was produced by snarkjs groth16 setup followed by a single contributor (scripts/circuit/build_pool.sh). For a real launch:
- Use a Powers-of-Tau ceremony output of size ≥ 2^19 (224K constraints requires
2^N >= 2*223513, fits in 2^19). - Coordinate a multi-party Groth16 phase-2 ceremony with N independent contributors (50–100 is typical for shielded-pool deployments). Any one honest contributor suffices for soundness.
- Compute the SHA-256 of the resulting
pool_vk.binand pin it in the spec'spool_vk.sha256asset; this is the build input the system contract bytecode is compiled against. - Recompile the system contract from that ceremony's verification key and pin the resulting bytecode at
SHIELDED_POOL_ADDRESSvia the activation-fork state dump. The install-time integrity check (Section 5.5) is on the bytecode digest, not on an extractable VK blob — Solidity-compiled verifiers split the VK across non-contiguous PUSH32 immediates rather than storing it as a contiguous slot.
CC0-1.0 (matches the EIP itself).