Skip to content

0xFacet/eip-8182-reference-implementation

Repository files navigation

EIP-8182 Reference Implementation (Groth16/BN254 + split-proof)

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.

Layout

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)

Asset bundle

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.sh

Without EIP_ASSETS_DIR the bundle stays in assets/eip-8182/ for local inspection.

Build prerequisites

  • Node 20+
  • Foundry (forge, anvil)
  • The vendored vendor/circom works on macOS arm64; on other targets, install circom 2.2.3 and point CIRCOM at it
  • For the Honk auth flow / npm run bench: nargo and Aztec's bb on PATH
  • 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 build

The 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).

End-to-end demo

node scripts/integration/build_session.js          # writes build/integration/session.json
node scripts/integration/build_honk_session.js     # optional: enables TransactHonkAuthTest + Honk benches
forge test

The 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 (calling ShieldedPool.verifyProof and the step-9 harness), DemoAuthVerifier, full transact against the accept-all harness, register/deposit/deregister flows.
  • TransactDemoAuth.t.sol — end-to-end transact with 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 seven test_bench_* benchmarks consumed by npm run bench.

Benchmarks

npm run bench

Builds 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:

  • Total is the full all-in tx gas: 21K base intrinsic + calldata (EIP-2028: 16/non-zero, 4/zero) + EVM execution gas. Not just the gasleft() 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.

Native pool prover

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.json

Observed 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.

What's normative vs reference

Normative (defined by the EIP, copied into assets/eip-8182/ and referenced from the spec):

  • pool.circom and 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.bin and its sha256 — pinned by EIPS/eip-8182.md Section 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.sol itself — 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 same transact interface as the demo. Companion ERCs deploy auth circuits permissionlessly per Section 11.

Trusted setup

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:

  1. Use a Powers-of-Tau ceremony output of size ≥ 2^19 (224K constraints requires 2^N >= 2*223513, fits in 2^19).
  2. 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.
  3. Compute the SHA-256 of the resulting pool_vk.bin and pin it in the spec's pool_vk.sha256 asset; this is the build input the system contract bytecode is compiled against.
  4. Recompile the system contract from that ceremony's verification key and pin the resulting bytecode at SHIELDED_POOL_ADDRESS via 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.

License

CC0-1.0 (matches the EIP itself).

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors