ByAUJay
Summary: Building a single Ethereum verifier that accepts both Groth16 and Plonk proofs is very doable in 2026 if you scope it correctly: target BN254 precompiles for lowest gas, route by proof type with strict field/length checks, and externalize large verifying keys to avoid the 24KB code size limit. If you also need BLS12‑381 Plonk, Pectra’s EIP‑2537 precompiles make it practical on mainnet—but you’ll still want a modular, upgradable “gateway + per‑scheme” design to manage keys, gas, and audits over time. (eips.ethereum.org)
How Hard Is It to Build a Verifier That Accepts Both Groth16 and Plonk Proofs on Ethereum?
Decision‑makers ask us this a lot: can we ship one contract that verifies both Groth16 and Plonk on Ethereum L1, with predictable gas and a clean developer experience? Short answer: yes—if you design around today’s precompiles, code‑size constraints, and the different ABI/cryptography of each system. Below we get concrete about what “hard” really means in 2026, what it costs, how to structure the contracts, and where teams still trip up.
The one‑paragraph version
- Ethereum L1 has cheap BN254 precompiles for EC add/mul and pairings (0x06–0x08), and a KZG point‑evaluation precompile at 0x0A (EIP‑4844). Pectra (May 2025) also added BLS12‑381 curve precompiles (0x0B–0x11). These make Groth16 on BN254 the cheapest thing to verify, and Plonk on BN254 or BLS12‑381 viable depending on your stack. (eips.ethereum.org)
- In practice, “both‑proofs” means a small router contract that dispatches to two verifier libraries (Groth16 and Plonk), plus disciplined key and calldata handling. Gas for a Groth16 verify is typically ≈200k–250k + ~7k per public input; Plonk verifiers land ≈300k+, depending on circuit and batching. (hackmd.io)
What “accepts both Groth16 and Plonk” really entails
“Both” is not just two functions. You must reconcile:
-
Curves and precompiles
- Groth16 on BN254 uses alt_bn128 precompiles at 0x06 (ECADD), 0x07 (ECMUL), 0x08 (ECPAIRING). EIP‑1108 cut costs to 150/6000 for add/mul and 45,000 + 34,000·k for k pairings. (eips.ethereum.org)
- Plonk relies on KZG polynomial commitments; you’ll see verifiers over BN254 and BLS12‑381. Ethereum exposes a KZG point‑evaluation precompile at 0x0A, and since Pectra, general BLS12‑381 precompiles at 0x0B–0x11. (eips.ethereum.org)
-
Proof and VK shapes
- Groth16: proof = (A ∈ G1, B ∈ G2, C ∈ G1); VK = (α1, β2, γ2, δ2, IC[]). Public inputs are a linear combination into G1 followed by a 3–4 pairing check. (eips.ethereum.org)
- Plonk: proof includes multiple commitments and opening proofs; VK holds selector commitments, permutation commitments, etc. The verifier does MSMs, computes challenges, then a few pairings/opening checks depending on the implementation. Tools like snarkjs and PSE/snark‑verifier generate Solidity verifiers for both. (github.com)
-
ABI differences
- Groth16 calldata is tiny (uncompressed ≈256 bytes; compressed ≈128 bytes), plus 32‑byte words per public input. Plonk proofs are typically 0.8–1.2 kB. Data bytes cost 4/16 gas (zero/non‑zero) under EIP‑2028, so ABI choices directly affect cost. (xn--2-umb.com)
The 2026 Ethereum surface area you’re building against
-
BN254 precompiles (since Byzantium), repriced by EIP‑1108
- ECADD 0x06 = 150 gas; ECMUL 0x07 = 6,000 gas; ECPAIRING 0x08 = 45,000 + 34,000·k gas. This is why Groth16 is so gas‑efficient on L1. (eips.ethereum.org)
-
KZG point evaluation precompile 0x0A (EIP‑4844)
- Fixed 50,000 gas to verify one evaluation against a commitment; defined input layout; this precompile underpins blobs and can be reused by verifiers that piggyback on KZG checks. (eips.ethereum.org)
-
BLS12‑381 curve precompiles (EIP‑2537, Pectra)
- 0x0B..0x11 add MSM and pairing support for BLS12‑381 with defined costs; important if your Plonk stack is already on BLS12‑381 and you prefer its higher security margin relative to BN254. (eips.ethereum.org)
-
Code size limit (EIP‑170): 24,576 bytes runtime code per contract
- Many Plonk VKs exceed this if embedded directly; you’ll store VKs externally or shard logic across libraries. (eips.ethereum.org)
Current, realistic gas you should budget
- Groth16 verify: ≈ 207,700 gas fixed + ≈ 7,160 gas per public input, matching empirical and analytic breakdowns of pairing + MSM + scaffolding. For 3 public inputs, ≈ 229–230k gas; large circuits with 20 inputs, ≈ 350k. (hackmd.io)
- Plonk verify (KZG, BN254 or BLS12‑381): ≈ 300k+ gas in typical on‑chain verifiers used in production SDKs today; exact numbers vary with gate set, selector compression, and batching. (docs.succinct.xyz)
These numbers include EIP‑1108 repricing and calldata at 4/16 gas per byte. If you compress Groth16 points (32B G1, 64B G2) you can halve proof bytes; whether that beats the extra compute depends on your calldata mix and gas prices, but audited implementations now make it a practical knob. (xn--2-umb.com)
High‑level architecture: two patterns that work
- Single “UnifiedVerifier” router + two internal verifiers
- Router function signature
- verify(bytes proof, uint8 proofType, uint256[] publicInputs) returns (bool).
- proofType ∈ {0 = Groth16, 1 = Plonk}.
- Internals
- Decodes and bounds‑checks, then dispatches to VerifierGroth16.verify() or VerifierPlonk.verify().
- Pros: simplest integration surface; one address to permission; easy to gate at app level.
- Cons: code size pressure; risk of ABI bloat; more frequent upgrades to a single address.
- Gateway + pluggable verifiers (what many rollups and zkVM SDKs use)
- A small “gateway” contract routes to registered verifiers per (scheme, version, vkeyHash).
- You can hot‑add/freeze verifiers and keep app logic fixed. This is the pattern used in widely deployed SDKs where both Groth16 and Plonk verifiers coexist behind one gateway. (docs.succinct.xyz)
Our recommendation for enterprises: ship the gateway pattern unless you have a one‑off, unchanging circuit/VK and tight audit budget.
A concrete, minimal Solidity shape
Below is a deliberately skinny sketch focusing on the engineering “edges” that matter. The real verifier math comes from codegen (snarkjs, gnark, snark‑verifier/halo2‑solidity, etc.), not hand‑written Solidity.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IGroth16 { function verifyProof(bytes calldata proof, uint256[] calldata pubInputs) external view returns (bool); function VKEY_HASH() external pure returns (bytes32); } interface IPlonk { function verifyProof(bytes calldata proof, uint256[] calldata pubInputs) external view returns (bool); function VKEY_HASH() external pure returns (bytes32); } contract UnifiedVerifier { error UnsupportedProofType(uint8 t); error PublicInputNotInField(); error WrongVKey(bytes32 expected, bytes32 got); // Wire up verifiers generated by your toolchain. IGroth16 public immutable g16; IPlonk public immutable plonk; // BN254 field modulus for public input checks uint256 constant FR_MOD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; constructor(address _g16, address _plonk) { g16 = IGroth16(_g16); plonk = IPlonk(_plonk); } function verify(bytes calldata proof, uint8 proofType, uint256[] calldata pubInputs) external view returns (bool) { // 1) Strict field checks (critical to avoid malleability) for (uint256 i = 0; i < pubInputs.length; ++i) { if (pubInputs[i] >= FR_MOD) revert PublicInputNotInField(); } // 2) Route by type + bind VK via hash (defense-in-depth against misrouting) if (proofType == 0) { // Optionally require a known vkey hash // bytes32 expected = <stored-or-hardcoded-hash>; // if (g16.VKEY_HASH() != expected) revert WrongVKey(expected, g16.VKEY_HASH()); return g16.verifyProof(proof, pubInputs); } else if (proofType == 1) { return plonk.verifyProof(proof, pubInputs); } else { revert UnsupportedProofType(proofType); } } }
Key points the sketch enforces:
- Public inputs must be canonical (strictly < field modulus). Modern audited verifiers expose this check because many production bugs came from implicit modular reduction. (gnosisscan.io)
- VK pinning via a hash/selector prevents “valid proof for the wrong circuit” from slipping through a shared interface, a pattern you’ll see in production verifier gateways. (docs.succinct.xyz)
Handling verifying keys and the 24KB limit
Even a modest Plonk verifying key can push you over EIP‑170’s 24KB runtime limit if you embed it verbatim. Use one of these storage patterns:
-
SSTORE2 “pointer” contracts for large immutable byte arrays
- Store VK blobs in the code of companion contracts and EXTCODECOPY at runtime; cheap to deploy and read, and keeps your main verifier tiny. Mature libraries exist. (github.com)
-
Library split + DELEGATECALL
- Put heavy math and VK tables into library contracts; the router/gateway DELEGATECALLs into them. Helps with code reuse and audits.
-
Immutable pointers
- Keep only a short hash and an address; read VK bytes from SSTORE2 on demand and decode in memory.
Pro tip: generate two builds of the Groth16 verifier—one accepting compressed points and one uncompressed—so you can switch at deployment depending on L1 calldata dynamics without touching the core app. Audited compressors exist. (xn--2-umb.com)
Gas: where it really goes (and how to save it)
- Pairings dominate Groth16 verification cost: 45,000 + 34,000·k gas for k pairings. Typical Groth16 contracts do four pairings in one batch call. Use the batched precompile; don’t do multiple calls. (eips.ethereum.org)
- Per‑public‑input cost comes from one MSM into the IC base points plus calldata bytes. An analytical/empirical model that holds up in practice is ≈ 207.7k fixed + ~7.16k per public input. Use it when sizing budgets. (hackmd.io)
- Plonk: gas moves with how many MSMs and opening checks you end up doing on‑chain. Codegen that compresses selectors and minimizes pairings tends to land ~300k–350k. If you must shave more, consider VK layout in SSTORE2 and tighter Yul for EC ops. (docs.succinct.xyz)
- Calldata economics: EIP‑2028 charges 4 gas/byte for zeros and 16 for non‑zeros—small ABIs matter. Groth16 proofs are 256B uncompressed; Plonk ≈ 0.8–1.2kB. Pick compressed Groth16 if your non‑zero byte ratio is high and your compute headroom is safe. (eips.ethereum.org)
BN254 today, BLS12‑381 when you need it
- If your circuits are BN254‑native (Circom/snarkjs defaults, gnark on BN254), verify on BN254. It’s the cheapest path and battle‑tested. (github.com)
- If your stack is already on BLS12‑381 (some Halo2/Plonk deployments), Pectra’s EIP‑2537 opens a first‑class L1 path. You’ll still keep the same router shape—one module per scheme/curve—and a VK hash to bind the contract to the intended key. (eips.ethereum.org)
Security note for execs: one motivation for EIP‑2537 was the higher security margin of BLS12‑381 vs BN254. Many apps are fine on BN254, but where you need extra headroom (validator crypto, long‑horizon trust), BLS12‑381 is now a native option on L1. (eips.ethereum.org)
Tooling that already works (so you don’t reinvent it)
- snarkjs can emit Solidity verifiers for both Groth16 and Plonk and the ABI helpers to format calldata consistently. Useful for quick prototypes and CI. (github.com)
- PSE’s halo2/solidity‑verifier and other “snark‑verifier” codegens target BN254 KZG and are being used by teams shipping Halo2/Plonkish systems to EVM. Handy when you want BLS‑ or BN‑flavor Plonk with good gas/code size trade‑offs. (github.com)
- Production gateways (e.g., SP1) ship both Groth16 and Plonk verifiers behind a single contract interface, pin VK hashes, and expose versioned verifiers you can freeze. If you don’t need custom circuits, you can piggyback on these. (docs.succinct.xyz)
Practical gotchas we see in audits
-
Field‑range checks on public inputs
- Never silently mod‑reduce public inputs; reject non‑canonical values. Good templates throw on NotInField. (gnosisscan.io)
-
Pairing argument packing
- Always use a single call to 0x08 with packed pairs; multiple calls cost more and complicate reentrancy analysis. (eips.ethereum.org)
-
VK drift
- Pin VKs by hash/selector and consider an allow‑list at the router. Teams have shipped contracts that accepted valid proofs for the wrong circuit because the ABI was shared and VK wasn’t bound. Production gateways show how to do this cleanly. (docs.succinct.xyz)
-
Code size and initcode
- Watch the 24KB runtime limit and 48KB initcode limit in CI; keep VKs out of runtime code. If you must embed constants, audit for compiler‑inserted dead code that pushes you over. (eips.ethereum.org)
-
Calldata bloat
- For Groth16, consider compressed point inputs. For Plonk, don’t ship unused commitments or selector columns; regenerate verifiers when circuit config changes. (xn--2-umb.com)
A step‑by‑step build plan we recommend
- Choose curves and generators up front
- BN254 Groth16 + BN254 Plonk if you want lowest L1 cost and simplest ops.
- BN254 Groth16 + BLS12‑381 Plonk if you’re standardizing on BLS curves elsewhere. (eips.ethereum.org)
- Generate verifiers with stable toolchains
- snarkjs for Groth16/Plonk, or gnark + its Solidity generators, or PSE/snark‑verifier for Halo2/Plonkish. Keep generator version pinned in the repo. (github.com)
- Wrap them behind a tiny router/gateway
- Enforce: proofType byte, VK hash binding, strict field checks, and a replay‑safe ABI.
- Keep the router upgradable behind a timelock if you anticipate switching curves or regenerating VKs.
- Externalize VKs
- Store as SSTORE2 blobs; expose a view that returns the VK hash and, optionally, emits the full VK via event for indexers. (github.com)
- Measure gas with realistic calldata distributions
- Use Foundry tests to compute gas at 0/16 mix extremes and a mid case; track per‑public‑input slope for Groth16 (~7.1k each) and overall Plonk baseline (~300k). (hackmd.io)
- Security hardening
- Fuzz public input length/field range; prove that malformed inputs cannot cause out‑of‑bounds reads in your decoder.
- Assert one pairing call per verify; forbid internal state mutation (STATICCALL where possible).
- Pin addresses of precompiles and revert on unexpected return sizes.
How “hard” is this, really?
-
Timeline
- 1–2 weeks for a focused team to stand up a production‑grade router with both verifiers, assuming circuits/VKs are ready and you reuse codegen.
- Add 1–2 weeks for audits and gas hardening if you’re embedding compressed Groth16 or BLS12‑381 Plonk for the first time.
-
Engineering risk
- Low to moderate. The riskiest parts—pairings and MSMs—live in precompiles and generator‑emitted code. The bespoke logic is mostly routing, key handling, and ABI discipline.
-
Ongoing ops
- Expect to rotate VKs when circuits change; the gateway pattern makes this routine. Monitor calldata‑vs‑compute gas to decide if/when to flip Groth16 compression on or off.
Bottom line for decision‑makers
If you want one verifier endpoint that handles both Groth16 and Plonk on Ethereum L1, you can absolutely ship it in 2026 with mature tooling, predictable gas, and audit‑friendly patterns. Start on BN254 for cost, add BLS12‑381 when your cryptography or ecosystem standard demands it, and keep verifiers modular with explicit VK pinning. The result is a future‑proof verification layer your teams won’t have to rewrite every time a circuit or curve changes. (eips.ethereum.org)
References and specs worth bookmarking
- EIP‑1108 (BN254 repricing), EIP‑196/197 (BN254 precompiles), EIP‑4844 (KZG 0x0A), EIP‑2537 (BLS12‑381 precompiles), EIP‑170 (24KB limit). (eips.ethereum.org)
- snarkjs verifier generators for Groth16/Plonk; PSE halo2‑solidity‑verifier. (github.com)
- Gas baselines and formulas used here (Groth16 per‑input slope and fixed cost; Plonk typical on‑chain). (hackmd.io)
- Groth16 compressed point verifier templates and write‑ups. (xn--2-umb.com)
- SSTORE2 for cheap VK storage. (github.com)
If you’d like, we can share a stripped, auditable “unified verifier” repo with CI that fails on field/length mistakes, over‑size code, and pairing packing, plus Foundry tests that print gas across proof types and input counts.
Like what you're reading? Let's build together.
Get a free 30‑minute consultation with our engineering team.

