7Block Labs
Blockchain Technology

ByAUJay

Summary: So, if you’re thinking about building a single Ethereum verifier that can handle both Groth16 and Plonk proofs, it’s totally achievable by 2026 if you plan it right. Focus on targeting BN254 precompiles to keep gas costs low, and make sure to route by proof type with tight checks on field and length. Plus, externalizing those big verifying keys will help you dodge that pesky 24KB code size cap. If you also want to include BLS12‑381 Plonk, Pectra’s EIP‑2537 precompiles will make it work on mainnet. That said, a modular, upgradable setup with a “gateway + per‑scheme” design will be key for managing 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 often ask us: is it possible to ship a single contract that verifies both Groth16 and Plonk on Ethereum L1, all while keeping gas costs predictable and ensuring a smooth developer experience? The quick answer is yes--provided you design with today’s precompiles, code-size limitations, and the unique ABI/cryptography of each system in mind. Below, we’ll break down what “hard” truly means in 2026, what the costs look like, how to structure the contracts, and where teams usually face challenges.

The one‑paragraph version

  • Ethereum L1 has some budget-friendly BN254 precompiles for elliptic curve addition/multiplication and pairings (those are 0x06-0x08), plus a KZG point-evaluation precompile at 0x0A (thanks to EIP-4844). And don't forget, Pectra (coming in May 2025) also introduced BLS12-381 curve precompiles (0x0B-0x11). With these in play, verifying Groth16 on BN254 becomes super cost-effective, while Plonk on either BN254 or BLS12-381 could also fit into your setup, depending on what you’re working with. (eips.ethereum.org)
  • So, when we say "both-proofs," we’re talking about a small router contract that sends tasks to two different verifier libraries (Groth16 and Plonk). You’ll also need to handle keys and calldata carefully. Generally, gas costs for verifying Groth16 hover around 200k-250k plus about 7k for each public input. Plonk verifiers tend to be a bit pricier, sitting at around 300k or more, depending on your circuit and how much batching you're doing. (hackmd.io)

What “accepts both Groth16 and Plonk” really entails

Reconciling Both Functions

“Both” isn’t simply about two functions working side by side. It's about finding a way to harmonize them:

  • Curves and Precompiles

    • When it comes to Groth16 on BN254, we’re tapping into those alt_bn128 precompiles located at 0x06 for ECADD, 0x07 for ECMUL, and 0x08 for ECPAIRING. Thanks to EIP‑1108, the costs have dropped to 150 for addition and 6000 for multiplication, plus 45,000 + 34,000·k for k pairings. You can check out the details here.
    • On the flip side, Plonk is using KZG polynomial commitments, and you’ll notice verifiers working with BN254 and BLS12‑381. Ethereum has made it easier by exposing a KZG point‑evaluation precompile at 0x0A. And since Pectra, we now have general BLS12‑381 precompiles spanning from 0x0B to 0x11. Want to dive deeper? Take a look here.
  • Proof and VK shapes

    • Groth16: The proof is made up of (A ∈ G1, B ∈ G2, C ∈ G1), while the verification key (VK) consists of (α1, β2, γ2, δ2, IC[]). For public inputs, you’ve got a linear combination going into G1 followed by a pairing check that usually involves 3-4 pairings. If you want to dive deeper, check it out here.
    • Plonk: Here, the proof packs in a bunch of commitments and opening proofs, and the VK has selector commitments, permutation commitments, and more. When it comes to verification, the process involves a series of multi-scalar multiplications (MSMs), some challenge computations, and then a handful of pairings or opening checks, depending on how you roll with the implementation. You can easily generate Solidity verifiers for both formats with tools like snarkjs and PSE/snark-verifier.
  • ABI differences

    • When it comes to calldata, Groth16 is super compact--about 256 bytes when it's uncompressed and around 128 bytes when it's compressed. On top of that, you add 32 bytes for each public input. On the flip side, Plonk proofs usually sit between 0.8 and 1.2 kB. Keep in mind that with EIP-2028, data bytes hit your gas costs at 4/16 for zero/non-zero values, which means your ABI choices can really impact your expenses. Check it out here: (xn--2-umb.com)

The 2026 Ethereum surface area you’re building against

  • BN254 precompiles (introduced with Byzantium) got a gas price overhaul thanks to EIP-1108.

    • ECADD (0x06) now costs 150 gas; ECMUL (0x07) is set at 6,000 gas; and ECPAIRING (0x08) starts at 45,000 gas plus an additional 34,000·k gas. This is a big reason why Groth16 is super gas-efficient on L1. (eips.ethereum.org)
  • KZG Point Evaluation Precompile 0x0A (EIP‑4844)

    • This one's set at a fixed 50,000 gas for verifying a single evaluation against a commitment. It lays out the input structure nicely, and it's pretty cool because this precompile supports blobs and can be handy for verifiers that take advantage of KZG checks. You can check out more about it here.
  • BLS12‑381 curve precompiles (EIP‑2537, Pectra)

    • The range 0x0B..0x11 introduces MSM and pairing support for BLS12‑381, complete with specific cost definitions. This is especially relevant if your Plonk stack is already using BLS12‑381, as it offers a better security margin compared to BN254. You can find more about it here.
  • Code size limit (EIP‑170): You've got a cap of 24,576 bytes for runtime code per contract.

    • Just a heads up, a lot of Plonk verification keys (VKs) go over this limit if you try to embed them directly. So, it's better to keep those VKs stored externally or split your logic across libraries. Check it out here: (eips.ethereum.org)

Current, realistic gas you should budget

  • Groth16 verification: It’s around 207,700 gas plus about 7,160 gas for each public input you throw in there. This aligns pretty well with what we've seen in both empirical data and the analytical breakdowns of pairing, MSM, and scaffolding. So, if you’re dealing with 3 public inputs, you’re looking at roughly 229-230k gas. And for those larger circuits with 20 inputs, it jumps to about 350k. Check out more details here.
  • Plonk verification (using KZG, BN254, or BLS12‑381): Expect to shell out around 300k+ gas for the usual on-chain verifiers that are being used in production SDKs today. Just keep in mind that the exact figures can fluctuate depending on things like the gate set, selector compression, and how you’re batching. If you want to dive deeper, take a look at this link: docs.succinct.xyz.

These figures take into account EIP‑1108 repricing and set calldata at 4/16 gas per byte. If you manage to compress Groth16 points (that’s 32 bytes for G1 and 64 bytes for G2), you could potentially cut your proof size in half. Whether that’s more beneficial than the extra compute needed really depends on your calldata mix and gas prices. Luckily, with the latest audited implementations, this has become a practical option you can tweak. Check it out here: xn--2-umb.com.


High‑level architecture: two patterns that work

1) Single “UnifiedVerifier” Router + Two Internal Verifiers

  • Router Function Signature

    • verify(bytes proof, uint8 proofType, uint256[] publicInputs) returns (bool)
    • Here, proofType can be either 0 for Groth16 or 1 for Plonk.
  • Internals

    • The router decodes and checks the inputs, then it sends the verification process over to either VerifierGroth16.verify() or VerifierPlonk.verify().
    • Pros:

      • Super straightforward integration; you only deal with one address.
      • It’s easy to manage permissions from the app level.
    • Cons:

      • Could face pressure on code size.
      • There's a chance of ABI bloat.
      • You might end up needing to update the single address more often.

2) Gateway + Pluggable Verifiers (a common setup in many rollups and zkVM SDKs)

  • There's this neat little “gateway” contract that routes requests to registered verifiers based on the combination of (scheme, version, vkeyHash).
  • One of the cool features is that you can easily hot-add or freeze verifiers while keeping your application logic unchanged. This approach is actually used in several popular SDKs where both Groth16 and Plonk verifiers work together behind a single gateway. Check it out here!

Our recommendation for enterprises: go ahead and use the gateway pattern unless you’re dealing with a unique, unchanging circuit/VK and have a strict audit budget.


A concrete, minimal Solidity shape

Here’s a quick, no-frills sketch that zooms in on the engineering “edges” that really count. Just a heads up: the true verification magic happens in the code generation stage (think snarkjs, gnark, snark-verifier/halo2-solidity, and so on), not in any hand-written Solidity stuff.

// 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 need to be canonical, which means they must be strictly less than the field modulus. Nowadays, many audited verifiers are on top of this check, as it turns out a lot of production bugs have come from overlooking modular reduction. You can see more about it here.
  • VK pinning through a hash or selector is a smart move--it stops “valid proof for the wrong circuit” from sneaking in through a shared interface. You’ll notice this pattern in production verifier gateways. Check out the details here.

Handling verifying keys and the 24KB limit

Even a basic Plonk verifying key can easily take you past EIP-170’s 24KB runtime limit if you just stick it in as-is. Instead, check out these storage patterns:

  • SSTORE2 “pointer” contracts for those hefty immutable byte arrays

    • You can stash VK blobs right in the code of your companion contracts and use EXTCODECOPY when you need them. It’s super cost-effective to deploy and read, plus it keeps your main verifier nice and small. There are some solid libraries out there to help you out. (github.com)
  • Library split + DELEGATECALL

    • Move the heavy math operations and VK tables into separate library contracts. Then, have the router or gateway use DELEGATECALL to access them. This approach not only promotes code reuse but also makes audits a whole lot easier.
  • Immutable pointers

    • They just hold a short hash and an address; you fetch the VK bytes from SSTORE2 whenever you need them and decode them right in memory.

Pro tip: Create two versions of the Groth16 verifier--one that takes compressed points and another for uncompressed points. This way, you can easily switch between them during deployment based on how L1 calldata behaves, all without messing with the core app. There are audited compressors available to help with this. Check it out here: (xn--2-umb.com)


Gas: where it really goes (and how to save it)

  • Pairings are a big factor in Groth16 verification costs, coming in at 45,000 + 34,000·k gas for k pairings. Usually, Groth16 contracts handle four pairings in one batch call, so make sure to use the batched precompile instead of running multiple calls. Check out more about it here.
  • The cost per public input comes from a single MSM into the IC base points plus the calldata bytes. An analytical/empirical model that works well in practice is about 207.7k fixed plus around ~7.16k for each public input. It’s a handy way to size your budgets. You can read more on it here.
  • With Plonk, gas usage varies depending on how many MSMs and opening checks you do on-chain. If you use code generation that compresses selectors and keeps pairings to a minimum, you’ll typically end up around 300k-350k. If you need to cut costs even more, think about rearranging the VK layout in SSTORE2 and using tighter Yul for EC operations. More details are available here.
  • As for calldata economics, EIP‑2028 sets the charge at 4 gas per byte for zeros and 16 for non-zeros--so those small ABIs really count. Uncompressed Groth16 proofs are 256B, while Plonk is roughly 0.8-1.2kB. If you notice a high ratio of non-zero bytes and have some compute headroom, you might want to choose compressed Groth16. Check out the specifics here.

BN254 today, BLS12‑381 when you need it

  • If your circuits are built to work with BN254 (think Circom/snarkjs defaults or gnark on BN254), then go ahead and verify on BN254. It's the most cost-effective route and has stood the test of time. (github.com)
  • But, if you're already set up with BLS12‑381 (like some of those Halo2/Plonk setups), check out Pectra’s EIP‑2537. It provides a top-notch L1 solution. You’ll maintain that same modular structure--one module for each scheme/curve--and a VK hash to link your contract to the right key. (eips.ethereum.org)

Security Note for Execs

Just a quick heads-up: one of the big reasons behind EIP‑2537 is that BLS12‑381 offers a better security margin compared to BN254. While a lot of applications run just fine on BN254, if you're looking for that extra cushion--especially in areas like validator cryptography or long-term trust--BLS12‑381 is now a solid, built-in option on Layer 1. You can check out more details here.


Tooling that already works (so you don’t reinvent it)

  • So, snarkjs is pretty cool because it can generate Solidity verifiers for both Groth16 and Plonk, plus it offers ABI helpers for formatting calldata just right. This makes it a great tool for whipping up quick prototypes and for continuous integration. Check it out on GitHub!
  • PSE’s halo2/solidity‑verifier and other “snark‑verifier” code generators focus on BN254 KZG and are already being put to good use by teams building Halo2/Plonk systems for EVM. This is really handy if you’re going for a BLS or BN-style Plonk while keeping a good balance between gas fees and code size. Dive deeper on GitHub!
  • If you’re looking at production gateways like SP1, they come with both Groth16 and Plonk verifiers wrapped in a single contract interface, lock in VK hashes, and let you work with versioned verifiers that you can freeze. If your project doesn’t require custom circuits, you can totally make use of these pre-built options. Learn more over at Succinct Docs!

Practical gotchas we see in audits

  • Field-range checks on public inputs

    • Always make sure you don't silently mod-reduce public inputs; instead, outright reject any non-canonical values. Good templates should throw a NotInField error. (gnosisscan.io)
  • Pairing argument packing

    • Stick to a single call to 0x08 when using packed pairs. Making multiple calls not only costs more but also complicates reentrancy checks. (eips.ethereum.org)
  • VK Drift

    • Make sure to pin VKs using their hash or selector and think about setting up an allow-list right at the router. There have been instances where teams launched contracts that ended up accepting valid proofs for the wrong circuit, all because the ABI was shared and the VK wasn’t tied down. Production gateways are a great resource that demonstrate how to do this in a neat way. Check it out here: (docs.succinct.xyz)
  • Code size and initcode

    • Keep an eye on that 24KB runtime limit and the 48KB initcode limit in CI. It's important to stay clear of including VKs in your runtime code. If you absolutely need to embed constants, make sure to check for any dead code that the compiler might add, as it could push you over the limit. (eips.ethereum.org)
  • Calldata Bloat

    • When it comes to Groth16, think about using compressed point inputs. For Plonk, make sure you’re not shipping any unused commitments or selector columns; just regenerate your verifiers whenever the circuit configuration changes. (xn--2-umb.com)

A step‑by‑step build plan we recommend

  1. Pick your curves and generators early
  • Go with BN254 Groth16 + BN254 Plonk if you're looking for the lowest L1 costs and the easiest operations.
  • Opt for BN254 Groth16 + BLS12‑381 Plonk if you’re already committed to BLS curves in other areas. (eips.ethereum.org)
  1. Generate verifiers with stable toolchains
  • You can use snarkjs for Groth16/Plonk, or check out gnark along with its Solidity generators. For Halo2/Plonkish, PSE/snark-verifier should do the trick. Just make sure to pin the generator version in your repo. (github.com)

3) Wrap Them Behind a Tiny Router/Gateway

  • Enforce: proofType byte, VK hash binding, strict field checks, and make sure your ABI is replay-safe.
  • Tip: Consider keeping the router upgradable with a timelock in case you think you’ll need to switch curves or regenerate VKs down the line.
  1. Externalize VKs
  • Let’s save them as SSTORE2 blobs. We can set up a view that gives you the VK hash and, if you want, we can also emit the full VK through an event for indexers. Check it out here: github.com
  1. Measure gas with realistic calldata distributions
  • Leverage Foundry tests to evaluate gas usage at both the 0/16 mix extremes and a mid-case scenario. Keep an eye on the per-public-input slope for Groth16 (around 7.1k each) and the overall Plonk baseline (about 300k). (hackmd.io)

6) Security Hardening

  • Test the limits of public input length and range; make sure that invalid inputs can't lead to out-of-bounds reads in your decoder.
  • Each verify operation should have just one pairing call; avoid any changes to the internal state (use STATICCALL whenever you can).
  • Lock in the addresses of precompiles and make sure to revert if you get unexpected return sizes.

How “hard” is this, really?

  • Timeline

    • If you have a dedicated team ready to go, you’re looking at around 1-2 weeks to get a solid production-grade router up and running with both verifiers. Just make sure your circuits/VKs are all set and you’re reusing the existing codegen.
    • If you’re planning to embed compressed Groth16 or BLS12‑381 Plonk for the first time, throw in another 1-2 weeks for audits and gas hardening.
  • Engineering risk

    • Low to moderate. The parts that carry the most risk--like pairings and MSMs--are tucked away in precompiles and code generated by the generator. Most of the tailored logic revolves around routing, key handling, and sticking to ABI discipline.
  • Ongoing ops

    • Be ready to switch up VKs whenever the circuits change; that’s just how the gateway pattern works. Keep an eye on calldata vs. compute gas to figure out if and when to toggle the Groth16 compression on or off.

Bottom line for decision‑makers

If you're looking to set up a verifier endpoint that can manage both Groth16 and Plonk on Ethereum L1, you’re in luck! By 2026, you'll have access to top-notch tools, stable gas fees, and audit-friendly practices that make this totally doable. Start off with BN254 to keep costs down, and when your cryptography or ecosystem standards evolve, you can smoothly add BLS12-381. Plus, keeping your verifiers modular with clear VK pinning means you’ll have a verification layer that’s ready for the future--no need to rewrite everything every time a circuit or curve gets updated. Check out more details here: (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). You can check them out here.
  • If you’re looking for some snarkjs verifier generators for Groth16/Plonk, check out the PSE halo2‑solidity‑verifier on GitHub.
  • For gas baselines and formulas used in this context (like the Groth16 per-input slope and fixed costs, plus Plonk’s typical on-chain usage), take a peek here.
  • Need some Groth16 compressed point verifier templates and write-ups? You’ll find them here.
  • And don’t forget about SSTORE2 for inexpensive VK storage--details can be found on GitHub.

If you're interested, we can totally share a lean, auditable “unified verifier” repo with CI that flags issues like field/length errors, oversized code, and pairing packing problems. We can also include Foundry tests that show gas usage based on different proof types and input counts.

Like what you're reading? Let's build together.

Get a free 30-minute consultation with our engineering team.

7BlockLabs

Full-stack blockchain product studio: DeFi, dApps, audits, integrations.

7Block Labs is a trading name of JAYANTH TECHNOLOGIES LIMITED.

Registered in England and Wales (Company No. 16589283).

Registered Office address: Office 13536, 182-184 High Street North, East Ham, London, E6 2JA.

© 2026 7BlockLabs. All rights reserved.