7Block Labs
Blockchain Technology

ByAUJay

What’s the Current Best Practice for Aggregating Groth16 Proofs on Ethereum to Cut Gas Costs?


TL;DR for decision‑makers

  • If you’ve got existing BN254 Groth16 stacks that need to settle on Ethereum L1, it’s a good idea to use a production aggregator like Nebra UPA. This allows you to compress N proofs into a single one, which you can then verify on-chain. You’re looking at around ~300k gas for the base, plus a bit for per-proof metadata. The savings can be huge--think 8-10×+ when compared to verifying them one at a time. Check out more details here.
  • For those apps that are all about throughput and can handle some crypto-economic finality, consider offloading the verification to a restaked proof-verification layer like Aligned. You’ll get latency in the milliseconds range and cut your costs down by about 90-99%. On Ethereum, this translates to an aggregated BLS attestation (around ~113k gas) or a super low per-proof cost of about ~2100 gas at current batch rates. Learn more about how it works here.
  • If you’re working on greenfield circuits, now’s the time to rethink your curve choice post-Pectra. Ethereum has rolled out BLS12-381 precompiles (EIP-2537), and Groth16 over BLS12-381 is actually cheaper for pairing checks (32600·k + 37700 gas) compared to BN254. Just keep in mind that it does come with larger calldata per point, which might shift how you view the end-to-end cost. You can dive deeper into this here.
  • Don’t forget to keep those public inputs to a minimum--hash or pack them when you can! Also, make sure your verifiers are robust (think field-range checks and domain separation). Each extra public input can cost you around ~6-7k gas, and if those range checks aren’t done right, it can lead to some serious bugs. For more insights, check out this article here.

Why this changed in 2025-2026

  • So, the Pectra mainnet is set to kick off on May 7, 2025! With this activation, we’re getting BLS12‑381 precompiles (thanks to EIP‑2537) and a bump in calldata costs for those data-heavy transactions (shoutout to EIP‑7623). As calldata becomes more expensive, the benefits of aggregation really start to shine, and BLS12‑381 opens up new possibilities for pairing-based designs to work well on Layer 1. You can read more about it here.
  • On the other hand, verification layers are really coming into their own. Aligned’s mainnet-beta is now verifying thousands of proofs each second off-chain and just posting one statement on-chain. This means we could see savings of 90-99% and new latency/throughput profiles for ZK apps. Pretty exciting stuff! Check it out here.

The baseline: how much does a single Groth16 verify cost on L1?

A Groth16 verifier call on Ethereum can be broken down into a few main components:

  • Pairing precompile(s) + EVM scaffolding + calldata + MSM over public inputs.
  • When using BN254 (alt_bn128), EIP‑1108 has set the pairing gas costs at 34,000·k + 45,000. Most verifiers typically handle around 4 pairings, plus an additional ~6-7k gas for each public input related to the MSM. So, you can generally expect the total gas usage to fall somewhere between ~200k and ~230k for a couple of public inputs, with an extra ~7k for each additional input. Check out the details here: (eips.ethereum.org).

After Pectra, BLS12-381 (EIP-2537) comes with a pairing cost that's 32,600·k + 37,700, which is a better deal compared to BN254 for each pairing. However, it does use 128/256 bytes for encoding points (G1/G2) which can bump up the calldata. Whether BLS12-381 is a better option than BN254 for you really hinges on a few factors: the number of public inputs, how your calldata patterns look, and if you're able to tweak your circuits. Keep in mind that a lot of the older Groth16 circuits still use BN254. (eips.ethereum.org)

Key takeaway: Checking N individual Groth16 proofs on L1 takes a linear amount of time, and it can get pretty overwhelming pretty fast.


The three patterns that actually save you money in 2026

1) Recursive aggregation: one proof to rule them all (full L1 finality)

What it is:

  • Off-chain, there’s an aggregator that checks N Groth16 proofs and creates one single “aggregated” proof--sometimes using recursion--that confirms all N proofs. Then, on-chain, you only need to verify this one proof.
  • Services like Nebra UPA are already doing this for Groth16 (and other systems). They usually charge around 300k gas for each aggregated proof, plus a little extra for bookkeeping per proof. (docs.nebra.one)

Why it’s the current best default for L1 settlement:

  • You spread the L1 costs over multiple proofs, which shifts verifications from O(N) to just O(1) on-chain work.
  • Here are some real-world stats: teams are seeing a gas cost around 300-350k for the aggregated proof, plus just a few thousand gas for each proof that gets included (thanks to inclusion metadata). This results in savings of 8-10× or more, especially when the batches get even a little bit bigger. (docs.nebra.one)

How to Ship It Quickly:

  • Start by registering your Groth16 verifying key (VK) with the aggregator just once.
  • Go ahead and submit your proofs off‑chain (or you can do it through their contracts).
  • Your app contract will use a single on‑chain verification result (like checkProof or isVerified mapping). For all the details, check out the Nebra docs. (docs.nebra.one)

Where Recursion Comes From

So, here’s the scoop:

  • In the world of paper and production systems, a lot of SNARK verifications are getting streamlined through recursion or those SNARK-friendly inner-product arguments (think SnarkPack for Groth16). Filecoin really led the charge here on a large scale, and now we see services packaging these concepts into handy SDKs. Just a heads-up: on-chain SnarkPack verifiers haven’t really hit the mainstream on Ethereum yet. Most teams are opting for recursive SNARKs (like Halo2-KZG) to bundle several Groth16 proofs into a single proof that the EVM can verify without any hiccups. You can dive deeper into this here.

When to Prefer It:

  • You’re looking for L1 cryptographic finality (meaning the aggregated proof is verified right on L1).
  • You’re dealing with anywhere from 10 to 1000 Groth16 proofs during your settlement window and can handle a few extra seconds for off-chain aggregation.

What to expect:

  • Gas: Around 300k base, plus a few thousand gas for each proof you include for bookkeeping.
  • Latency: It can take anywhere from seconds to minutes to build up those big recursive proofs, and it tends to increase with the size of the batch and how much you’re running in parallel. Check out more details here!

2) Off‑chain verification + on‑chain BLS attestation (crypto‑economic finality, extreme throughput)

What it is:

  • A decentralized operator set, backed by restaked ETH, is in charge of running the verifier code directly. These operators use BLS to co-sign decisions, and you just need to verify a single aggregated signature or batch receipt on Ethereum. A great example of this is Aligned’s “Proof Verification Layer.” You can check it out here.

Why it’s compelling:

  • Aligned’s mainnet beta is really impressive, verifying about 200 proofs per second at around 2100 gas per proof, with plans to ramp that up to thousands in the future. The on-chain part consists of an aggregated BLS signature check, which costs roughly 113k gas, but with batching, that cost gets shared out across multiple items. This clever approach slashes verification costs by a whopping 90% to 99%, plus it keeps latency down to just milliseconds at the verifier layer. You can dive into more details here.

Trade-off:

  • You get that EigenLayer-style crypto-economic security (and guess what? Slashing is now up and running) instead of having to rely on direct L1 cryptographic verification for each proof. A lot of use cases, like feeds, oracles, and those “soft finality” flows, can totally work with this setup. However, some folks, especially when it comes to settlement-critical rollups, might still prefer having a periodically-posted recursive aggregated proof for that hard finality. (coindesk.com)

When to Prefer It:

  • If you're after the lowest gas costs per proof and top-notch latency, and you're okay with some crypto-economic guarantees.
  • You’ve got the option to team it up with recursive aggregation (thanks to Aligned’s Aggregation Service) when you need L1 finality for your batches. Check it out here!

3) “Do nothing clever” (verify N Groth16 proofs one by one)

Only makes sense if:

  • N is pretty small and you can stick to just 1-2 public inputs.
  • If not, you're looking at about ~200k-230k gas for each proof, plus around ~6-7k for every public input--that adds up fast and can easily blow through block gas limits even for a decent N. (medium.com)

Concrete cost picture (with up‑to‑date numbers)

  • BN254 Groth16 Verification on Ethereum:

    • Pairings: You're looking at about 34,000·k + 45,000 gas (thanks to EIP‑1108). Most common verifiers use 4 pairings, which means you're hitting around 181k gas just for those pairings. Check it out here.
    • MSM Over Public Inputs: It’ll cost you roughly 6,150-7,160 gas per input.
    • Typical End-to-End: If you're working with 2-3 public inputs, expect it to run around 207-230k gas; and for each extra input, toss in an additional ~7k. For more details, see this article.
  • BLS12‑381 Groth16 after Pectra:

    • Pairings: You’re looking at about 32,600·k + 37,700 gas for each pairing. To break it down a bit more, if you're doing 4 pairings, it’ll cost around ~168k gas, and for 3 pairings, you’ll need about ~135.5k. Keep in mind that the calldata gets a bit bigger with each point (we’re talking 64-byte limbs), so the overall cost really depends on how you encode things and your public input approach. (eips.ethereum.org)
  • Aggregation services:

    • Recursive aggregation (Nebra UPA): It takes about ~300k gas for each aggregated proof, plus a little overhead per proof. That means you can save over 10 times at decent batch sizes! Right now, it supports Groth16. Check out more details here.
    • Verification layer (Aligned): Currently, this one costs around ~2100 gas per proof at the existing batch rates. When it comes to aggregated BLS signatures, we're looking at about ~113k gas. It supports gnark Groth16 BN254 and a few others. You can read more about it here.
  • Calldata costs have shot up for data-heavy transactions thanks to EIP-7623. This change makes it even less appealing to post a bunch of raw proofs compared to aggregating them. (eips.ethereum.org)

Implementation playbooks (what to do this quarter)

A) You already produce BN254 Groth16 (Circom/gnark) and need L1 finality

  • Integrate a recursive aggregator (like Nebra):

    1. Sign up for your VK once and grab your circuit/app ID.
    2. Send your proofs off-chain and keep an eye out for those “verified” events.
    3. In your app contract, pull in the aggregated verification result (just map it by proof ID) and move forward. (docs.nebra.one)
  • Engineer for batching: Figure out your batch windows based on SLA (think 10-60 seconds) to save on gas costs while making sure you stick to your latency SLOs.

What to Expect

You can expect around 300-350k gas for each aggregation, plus a little extra for per-proof overhead. If you were looking at a cost of about 220k per unit multiplied by N, then your breakeven point is pretty minimal. Check out the details here!

B) You’re throughput/latency bound and can accept crypto‑economic guarantees

  • Check out a verification layer (Aligned):
    • It’s pretty cool because it backs gnark Groth16 (BN254) and more, allowing you to verify thousands of proofs per second right out of the box. To give you an idea, Ethereum currently sees an aggregated BLS signature that costs around ~113k gas, and each proof runs about ~2100 gas at current settings. You can read more about it here.
    • Plus, if you want, you can route the same proofs later into an Aggregation Service. This way, you can get a single recursive proof posted to Layer 1 for that extra layer of finality. More info on that can be found here.

C) Greenfield circuits (new builds)

  • Take another look at your curve target after Pectra:
    • The BLS12‑381 precompiles are up and running! The gas for pairing is cheaper compared to BN254, plus we've got MSM precompiles available. If you're able to generate Groth16 on BLS12‑381 from start to finish, it could mean L1 verification is not just cheaper, but also more secure (think 128-bit+). Just be sure to benchmark the total gas including calldata before you dive in. (eips.ethereum.org)
  • Don’t forget about aggregation: even with BLS12‑381, if you're verifying N individual proofs, it’ll scale linearly; aggregation is your best bet for saving 10× or more.

D) Cut your per‑proof gas today (with or without aggregation)

  • Minimize public inputs: Instead of passing multiple public signals, hash and pack them all into one field element and use that as your only public input. As a rule of thumb, you can save around ~6-7k gas for each input you drop. A solid approach is to compute Poseidon/Keccak within the circuit and just verify the digest on-chain. (medium.com)
  • Use a 3-pairing verifier where safe: Although Groth16 theoretically requires 3 pairings, some templates simplify things by using 4. If you stick with a 3-pairing verifier, you could save about ~34k gas on BN254. Just make sure to validate against your generator and perform security checks before deploying. (xn--2-umb.com)
  • Export optimized verifiers: Tools like gnark/snarkjs can help you export Solidity verifiers. Try to keep VK points immutable or constant whenever you can to dodge those SLOAD costs. (docs.gnark.consensys.io)

Practical, numbers‑first example

Imagine you’re cranking out 256 Groth16 proofs every minute, and each one has 2 public inputs.

  • Naïve L1 verification (BN254):

    • Each proof takes about ~220k gas, and with 256 proofs, we’re looking at around 56.3M gas per batch. That’s way over the 45M gas limit for a block! So, we'd end up needing multiple blocks, which just drives up fees and adds latency. Check out more about it here: (medium.com)
  • Recursive aggregation (Nebra‑style):

    • You’re looking at about ~300k gas for the aggregated proof plus around ~7k for each proof's metadata. That totals roughly 300k + 1.8M = 2.1M gas. This fits nicely within a single block, leaving you with a comfortable 25× headroom. Plus, it helps cut down on your fee exposure and speeds up confirmation time. Check it out here: (docs.nebra.one)
  • Verification layer (Aligned):

    • On-chain, there's just one aggregated BLS check (costing about ~113k gas) that takes care of the whole batch. Meanwhile, off-chain operators have verified all 256 proofs. This means your “per-proof” cost sits around ~2100 gas with the current batch rates, and you can expect verification latency in the milliseconds before the attestation goes live. Check out more details here!

Security and correctness checklist (don’t skip this)

  • Field-range checks for public inputs: Always make sure to check each public signal against the scalar field r to dodge any input aliasing issues. Some of the older templates and libraries have had their fair share of bugs, so it’s a good idea to stick with the latest generators and add some explicit checks if necessary. (security.snyk.io)
  • Domain separation: Tie your proof to a circuit ID, deployment chain ID, and the application context. This step helps prevent any cross-circuit replay situations.
  • Immutable VK and circuits: Lock down your VK parameters. If you think you’ll need to upgrade things later, consider setting up some timelocks and clear governance paths for migration.
  • Aggregation-specific:
    • When it comes to verification layers, make sure the attested statement has all the essential bindings (proof IDs, VK IDs, public input digests, and your app domain).
    • For recursive aggregation, take a close look at the outer circuit’s verifier gadget and transcript; recursion bugs can really mess with soundness.
  • Post-Pectra encoding: The BLS12-381 precompiles expect 64-byte limbs, so double-check the ABI packing and endianness. Mismatches often lead to reverts, and you definitely want to avoid that! (eips.ethereum.org)

Engineering tips we use in client projects

  • Pack/commit public inputs:

    • When you're inside the circuit, calculate h = Hash(publicSignals) and just share h as public. On the blockchain, you'll want either one uint256 (BN254) or two 256-bit words (thanks to those BLS12-381 limb encoding requirements) to save around 6-7k gas for each original input. You can read more about it here.
  • Precompute calldata off-chain:

    • Make use of library helpers to generate calldata that’s ready for the verifier. Try to steer clear of loops that push array elements one by one; it can bog things down.
  • Keep verifier calls pure and small:

    • When you're verifying, avoid any storage writes. Just check things and return a boolean. If a proof is invalid, use branching to skip unnecessary work instead of doing follow-up writes.
  • Parameterize batch windows:

    • For those aggregators out there, make sure you have a config option that lets you tweak the batch size or time window without needing to redeploy your contracts. Adjust it based on your service level agreement (SLA) and the current gas market conditions.

Minimal Solidity Scaffold (Packing Public Inputs to a Single Field Element and Calling a Verifier)

This minimal Solidity scaffold is designed to streamline the process of packing public inputs into a single field element and then calling a verifier. Let’s dive in!

Solidity Code

Here’s a simple way to handle the packing of public inputs:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MinimalScaffold {
    uint256 public constant FIELD_SIZE = ...; // define your field size here

    function packInputs(uint256[] memory inputs) public pure returns (uint256) {
        uint256 packedInput = 0;

        for (uint256 i = 0; i < inputs.length; i++) {
            packedInput |= (inputs[i] & ((1 << FIELD_SIZE) - 1)) << (i * FIELD_SIZE);
        }

        return packedInput;
    }

    function callVerifier(uint256 packedInput) external {
        // Assume verifier contract is deployed at `verifierAddress`
        Verifier verifier = Verifier(verifierAddress);
        require(verifier.verify(packedInput), "Verification failed");
    }
}

interface Verifier {
    function verify(uint256 packedInput) external returns (bool);
}

Explanation:

  1. Packing Inputs: The packInputs function takes an array of public inputs and packs them into a single uint256. This helps to keep things neat and tidy when you want to handle inputs.
  2. Calling the Verifier: The callVerifier function is where the magic happens. It takes the packed input and calls a verifier contract. Just make sure to replace verifierAddress with the actual address of your verifier contract.

Important Notes

  • Make sure to adjust the FIELD_SIZE constant according to your specific use case.
  • The Verifier interface can be modified based on what the verifier contract requires.

This scaffold gives you a solid starting point for handling public inputs efficiently. Happy coding!

// pseudo-interfaces; replace with your generator’s types
interface Groth16Verifier {
    function verifyProof(
        uint256[2] calldata a,
        uint256[2][2] calldata b,
        uint256[2] calldata c,
        uint256[] calldata publicInputs
    ) external view returns (bool);
}

contract UsesProof {
    Groth16Verifier public verifier;
    bytes32 public expectedDigest; // set by admin or computed off-chain

    constructor(address _verifier, bytes32 _expectedDigest) {
        verifier = Groth16Verifier(_verifier);
        expectedDigest = _expectedDigest;
    }

    // Only one public input: digest mod r
    function submit(
        uint256[2] calldata a,
        uint256[2][2] calldata b,
        uint256[2] calldata c,
        bytes32 digest
    ) external view returns (bool ok) {
        // Optional: explicit field-range check here (digest % r == digest)
        uint256 x = uint256(digest); // pack into field element off-chain too
        require(digest == expectedDigest, "bad digest");
        uint256[] memory inputs = new uint256[](1);
        inputs[0] = x;
        ok = verifier.verifyProof(a, b, c, inputs);
    }
}

Link this into an aggregator by saving proof IDs and referencing the aggregator’s “verified” mapping instead of reaching out to the verifier directly.


Emerging practices to watch

  • BLS12‑381 Groth16 end‑to‑end: Now that EIP‑2537 is live, we can expect more teams to hop on the BLS12‑381 train for their new circuits. The pairing gas is lower, MSMs come with native precompiles, and there’s a better security margin compared to BN254. Just keep in mind that your overall costs will depend on how you balance calldata and compute; it's worth benchmarking based on your circuit sizes. (eips.ethereum.org)
  • Calldata economics are tightening: With EIP‑7623 raising costs for data-heavy transactions, we’re seeing proposals like EIP‑7976 suggesting even more floors. This makes aggregation even more valuable as the price of calldata keeps climbing. (eips.ethereum.org)
  • Unified flows: A lot of teams are getting smart by combining both modes--using a verification layer for fast, affordable verification to boost user experience, along with periodic recursive aggregated proofs to ensure L1 hard finality (think every M minutes or after K proofs). (blog.alignedlayer.com)

Bottom line (what we recommend at 7Block Labs)

  • Need L1 finality for a bunch of Groth16 proofs? Check out recursive aggregation, like Nebra UPA. This approach will get you around the ~300k gas per batch zone, and it scales pretty smoothly. (docs.nebra.one)
  • If you’re dealing with throughput or latency issues but can roll with some crypto‑economic guarantees, consider using a verification layer like Aligned. You could save 90-99% on costs and achieve verification in just milliseconds, plus you can throw in some periodic recursive proofs if you want. (blog.alignedlayer.com)
  • For any new builds, think about generating Groth16 proofs using BLS12‑381 and verifying them with EIP‑2537 precompiles; remember, aggregation still gives you the edge when scaling up. (blog.ethereum.org)
  • No matter which route you choose, make sure to minimize public inputs, enforce field-range checks, and domain-separate everything. These steps are easy to implement but can lead to significant savings and reduce risks. (medium.com)

If you’re interested in a solid feasibility study based on real numbers for your circuit sizes and traffic profile, we can dive into the gas/latency trade-offs between BN254 and BLS12-381, as well as explore recursive aggregation versus verification layers. We'll also put together a robust verifier/aggregator integration plan and have it ready for you in just two sprints.

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.