ByAUJay
Summary: When you're working with Ethereum L1, you can't really get a Groth16 verification down to less than 100k gas. That's mostly because the bn254/BLS12 pairing precompiles are already above that threshold. If you want to hit that “<100k gas per proof” mark in real-world scenarios, you’ve got a couple of options: you can either batch verify on-chain using a random-linear-combination method or you can offload the verification to a dedicated proof-verification layer and use an aggregated BLS signature to attest.
What’s the Cleanest Way to Optimize an On‑Chain Groth16 Verifier So Each Proof Costs Under 100k Gas?
Decision-makers often hit us up with this common question while budgeting L1 costs: “Can we get our Groth16 verifies under 100k gas?” To keep it short and sweet: if you're on the Ethereum mainnet, you can’t really do it for a single, stand-alone verify. The pairing precompile sets a strict minimum that’s already over 100k. However, if you play your cards right with the architecture, you can definitely keep it below 100k per proof on a regular basis. You can do this by bundling a bunch of verifies into a single on-chain check or using a verification layer that confirms it on-chain.
Here’s the detailed math, what's new since 2025 (thanks to Pectra), and the smoothest implementation paths we've rolled out for our clients.
The hard lower bound: pairing precompile math you cannot out‑optimize
- When it comes to BN254 (alt_bn128) after EIP‑1108, the pairing check precompile at 0x08 sets you back 45,000 + 34,000·k gas. Here, k represents the number of G1×G2 pairs you pass in a single multi‑pairing. Typically, Solidity Groth16 verifiers handle four pairs in one call (−A with B, plus three constant/product terms), so just the pairing itself can cost around 181,000 gas. And remember, that’s just for the pairing--you're not factoring in the input MSM and scaffolding yet. There's no way to get around this minimum cost by trying to "inline" or micro-optimize things. (eips.ethereum.org)
- In theory, Groth16 requires three pairings, but the typical Ethereum templates actually send four pairs to the precompile to carry out the product-equals-identity check in a single call. So, when you're budgeting, it's better to plan for k=4. (alinush.github.io)
- As of May 7, 2025, the Ethereum mainnet rolled out BLS12‑381 precompiles (thanks to EIP‑2537, which came with Pectra). If you’re curious about the pricing for BLS pairings, it’s set at 37,700 + 32,600·k gas. That means if you’re looking to do three pairings, you’re looking at 135,500 gas, and four will set you back 168,100 gas--both still over that 100k mark. So, while this opens up more options for your curves, it doesn’t really change the base floor. You can read more about it here.
- For a single Groth16 verification (like on BN254), you can expect empirical gas totals to be around 200k to 250k, and this can vary based on the number of public inputs. The multi-scalar multiplication (MSM) needed to compute vk_x tacks on about 6.15k gas for each public input, thanks to those EC MUL/ADD precompiles. A commonly referenced rough estimate is around 207,700 plus 7,160 times ℓ gas for ℓ public inputs. (hackmd.io)
Conclusion
In the end, a stand-alone on-chain Groth16 verification on Ethereum L1 isn’t going to dip below 100k gas. If you want to reach that goal, you really need to think about amortizing or using attestation.
Two clean paths to “<100k gas per proof”
Path A -- On‑chain batch verification (random linear combination)
Batch verification lets you verify multiple proofs all at once, and you’ll only need about n+2 pairings instead of the usual 3-4 for each proof. Since pairings tend to be the bottleneck, when you distribute those across n proofs, the cost per proof drops below 100k really fast.
- Here’s the deal: you generate a challenge ( r ) from your batch (think Fiat-Shamir), and then check to see if the sum of the scaled equations adds up. When it comes to Groth16, you can simplify things by cutting down n checks to just one multi-pairing, plus about ( O(n) ) G1 MSMs. If you’re working with Ethereum, this means you’ll make one pairing call with ( k \approx n + 2 ) and you’ll need n MSMs for ( vk_x ). (fractalyze.gitbook.io)
- Gas model on BN254:
- Pairing: 45,000 + 34,000·(n+2)
- MSM for inputs: around 6,150·ℓ per proof
- Overhead: just a few thousand gas for calldata, bounds checks, and the STATICCALL itself
- As n increases, the amortized cost for each pairing per proof comes down to about 34k. So, if you've got one public input (ℓ=1), you're looking at a total of roughly 34k + 6.15k + overhead, which lands you in the ballpark of 45k-60k. If ℓ=3, it bumps up to about 52k-70k. Generally, you can keep it under 100k when n is between 4 and 8, unless your public IO is on the larger side. (eips.ethereum.org)
- When it comes to the randomness source, r needs to be totally unpredictable for provers while they're putting together the batch. Here’s a solid approach on Ethereum:
- Generate r like this:
r = keccak256(abi.encode(block.prevrandao, msg.sender, keccak256(allProofBytes), vkHash)). - If submitters can mess with prevrandao (think small L2s), it’s a good idea to use a commit-reveal scheme or get r from a VRF or some logically prior commitment. You can check out more details here.
- Generate r like this:
- When to use: This is handy when you’re managing a bunch of proofs that all point to the same verification key (VK), like for wallet privacy sets, allowlist memberships, or small business logic circuits. Just keep in mind that a few seconds of batching latency is totally fine in these cases.
Practical note: Just a heads up, you'll still need to put in O(n) work to compute vk_x on-chain. It's a good idea to keep ℓ (the number of public inputs) pretty small to prevent shifting the bottleneck from pairings to MSM. (hackmd.io)
Path B -- Off‑chain verification + on‑chain BLS attestation (verification layer)
If you're okay with using economically secured attestation instead of just straight-up verification in your contract, consider moving those proof checks off-chain and then posting a BLS-signed verdict to Ethereum.
- Nowadays, we have these cool “verification layers” like the Aligned Layer that can check any SNARK/STARK proofs off-chain and bundle operator signatures together. When we’re on Layer 1, your contract just needs to verify one aggregated signature and pulls in the batch results. The on-chain costs usually sit at about 350k gas for each batch plus roughly 40k gas for each proof, assuming a batch size of 20. That’s pretty efficient--definitely below that 100k mark! Check it out here: (blog.alignedlayer.com)
- Thanks to Pectra’s BLS12‑381 precompiles, checking an aggregate BLS signature is super efficient--you're looking at just two pairings along with a bit of hashing/MSM, which totals about 103k gas for the pairing part. And when you spread that cost out over a batch, it goes way down to well under 100k per proof. (eips.ethereum.org)
- When to use: if you’re in a pinch and need low per-proof gas right away, and you can rely on a financially secure committee (like the EigenLayer-backed operators in Aligned’s scenario), plus you’re looking for super quick verification latency (like, milliseconds) along with batched settlement on Ethereum.
Micro‑optimizing your Solidity verifier still matters (and how to do it cleanly)
Even if you’re batching or attesting, you’ll still need those on-chain verifiers around (just in case for fallbacks, spot checks, or Layer 2 solutions). Here’s the lowdown on how to cut down on unnecessary gas units without messing up the correctness.
- Use the precompiles wisely and just once
- For BN254, you'll find the addresses at: 0x06 (ECADD), 0x07 (ECMUL), and 0x08 (PAIRING). Make sure to call PAIRING exactly one time with all your pairs; if you call it multiple times, you'll end up paying that base 45k fee each time. (eips.ethereum.org)
- When it comes to BLS12‑381 addresses (available since Pectra): check out 0x0b through 0x11 for G1/G2 add, MSM, pairings, and maps. If you're working with BLS12‑381, take advantage of the MSM precompiles to calculate vk_x more efficiently instead of doing repeated ECMUL+ECADD. (eips.ethereum.org)
- Keep the verification key away from storage
- It's a good idea to hard-code VK elements as immutables or constants. Try to steer clear of SLOADs. Using snarkjs-style templates that treat IC as memory constants and dodge dynamic arrays is already a win; making IC a fixed-size array can help save on those pesky bound checks. (github.com)
3) Always Perform Mandatory Bound Checks in Solidity Before Hitting Precompiles
To avoid potential issues like malleability and “input aliasing,” it's super important to do some checks:
- Make sure all G1/G2 coordinates of A, B, and C are less than ( p ) (the modulus for the curve base field).
- Check that all public inputs are below ( r ) (the modulus for the scalar field). There have been instances where code generators focused on ( p ) instead of ( r ), which allowed for those sneaky aliasing exploits. If you're using generated verifiers, definitely audit this part! (github.com)
- Keep memory expansion and calldata copies to a minimum
- Try using assembly to create the pairing input buffer all in one go with STATICCALL. Avoid making new arrays inside loops. Instead, pre-size your buffers and reuse your scratch space. This little tweak has been known to save a few thousand gas per verification based on our findings. You can check out the reference implementations in “Pairing.sol” to see the single-call pattern in action. (rareskills.io)
- Let's crunch vk_x with the least amount of operations
- When it comes to BN254, you can think of vk_x as IC0 plus the sum of each xi multiplied by ICi. This ends up costing around (6000 + 150)·ℓ gas after EIP‑1108 rolls out. To keep things efficient, try using tight loops and unrolled additions for smaller values of ℓ. For BLS12‑381, it's smarter to go with G1MSM (0x0c) instead of doing k separate ECMUL calls. The precompile’s Pippenger-style optimizations really pay off when you have k≥2. (eips.ethereum.org)
- Pack calldata tightly and skip on-chain decompression
- Precompiles are designed to handle uncompressed big-endian coordinates: for BN254 G1/G2, you’ll need 64/128-byte pairs, while BLS12‑381 uses 128/256 bytes for G1/G2. It’s best to avoid sending compressed points and decompressing them on-chain; that just racks up gas costs and opens the door to errors. (eips.ethereum.org)
7) Be Chain-Aware: Gas Schedules Vary Outside Ethereum L1
- When you're working with different chains and Layer 2 solutions, keep in mind that they often have their own twists on precompile pricing. It's a good idea to avoid hardcoding the gas stipend for these pairings. Take zkSync, for instance--they adjusted the ECADD/ECMUL/PAIRING pricing through ZIP-11. There have been audits that uncovered issues stemming from hardcoded limits of 120k for k=2 pairings. To stay on the safe side, make the gas you send to STATICCALL a parameter, or better yet, calculate it based on k. Check it out here: github.com
Putting numbers on it: three worked examples
1) Single Groth16 Verify on Ethereum L1 (BN254), ℓ=3
- Pairing (k=4): 181,000 gas
- MSM for inputs: about 6,150 × 3, which comes to around 18,450 gas
- Calldata + scaffolding: roughly 4-8k gas
- Total: around 205-210k gas. This aligns pretty well with what we often see in production. (eips.ethereum.org)
Batch 16 Groth16 Proofs on Ethereum L1 (BN254), ℓ=1 Each
- For one pairing call, we’ve got k≈n+2=18, which adds up to 45,000 + 34,000×18 = 657,000 gas.
- When it comes to MSM across the 16 proofs, we’re looking at about ≈6,150×16 = 98,400 gas.
- There’s also some overhead to consider: roughly ≈15-25k gas.
- So, the total for the batch comes out to around ≈770-780k gas, meaning each proof is about ≈48-49k gas. That’s well below 100k! And if you throw in ℓ=3, you’re still sitting at around ≈60-70k per proof. Check out more details on EIP 1108.
3) Verification Layer Attestation (BLS12‑381)
- Off-chain: Operators can check any proof system.
- On-chain: Verify a single aggregate BLS signature and then update the batch state.
As for the costs, it's reported that each batch costs around 350k gas as a base, with about 40k gas needed per proof when the batch size is around 20. The BLS precompiles set up by Pectra help keep this predictable on the mainnet. You can dive deeper into it here: (blog.alignedlayer.com)
BN254 vs BLS12‑381 in 2026: what changed and what didn’t
- BN254 continues to be the go-to choice for affordable Groth16 on L1. This is mainly because EIP‑1108 cut down the costs for ECADD, ECMUL, and PAIRING a while back. That's why you'll see so many Ethereum-focused circuits still compiling to BN254. (eips.ethereum.org)
- With Pectra launching on May 7, 2025, BLS12‑381 has officially become a first-class citizen on Ethereum. It comes with seven precompiles, including MSM and field-to-curve mappings. If your ecosystem is already on board with BLS12‑381--think validator tooling, light clients, and BLS wallets--you can easily switch your verifier curve without facing a hefty gas penalty. In fact, using the BLS pairing actually costs slightly less per pair compared to BN254. However, keep in mind that the larger point encodings and varying MSM economics can sometimes balance the totals out. Be sure to evaluate everything from start to finish with your specific ℓ and calldata patterns. (blog.ethereum.org)
- Calldata pricing is important: With the introduction of EIP‑7623 alongside Pectra, the cost of calldata for transactions that use a lot of data has gone up. On the bright side, Groth16 proofs are pretty compact (around 256-300 bytes), so the effect isn't huge. However, this change makes it tougher for "fat proof" schemes on Layer 1 and emphasizes the benefits of batching and attestation. (blog.ethereum.org)
A clean implementation blueprint you can ship
If you’re managing both the prover and consumer contracts, you can consistently keep this architecture under 100k per proof without needing any fancy cryptography:
- Make public inputs small
- Revamp the circuit API to keep ℓ to a minimum (consider packing, hashing, or committing to the application state off-chain while only exposing essential scalars). Just so you know, each public input hits you with about 6.15k gas on BN254. (hackmd.io)
2) Implement Batch Verification with Non-Malleable r
Let’s add a verifyBatch() function that’s pretty straightforward:
- It should take an array of tightly encoded proofs and their corresponding public inputs for a single VK.
- To derive
r, you’ll use this formula:r = keccak256(prevrandao || vkHash || keccak256(batchBytes)). - You’ll want to compute all the
vk_xvalues in one tight loop for efficiency. - Then, prepare one pairing input buffer that has about
k≈n+2pairs. - Finally, you’ll call the pairing precompile with a
STATICCALLjust once.
In case things go sideways, make sure to fail the entire batch if the check doesn't pass. If you need to identify exactly which proof caused the issue, you can include per-proof bitmaps, but keep in mind that this will use extra gas. (fractalyze.gitbook.io)
3) Harden the Verifier
- Make sure to reject any coordinates that are greater than or equal to
p, and any public inputs that are greater than or equal tor, before you call the precompiles. This step helps guard against malleability and aliasing. Also, ensure that you're using fixed-length IC arrays and avoid dynamic allocations on those crucial hot paths. Check it out here: (github.com)
4) Make Gas Portable
- Calculate the pairing gas stipend from ( k ) instead of hardcoding it. You can either make it a constant or create a function that takes the input length into account. This way, you can steer clear of cross-chain/L2 issues that might pop up when the precompile pricing varies. (github.com)
- If latency or multi‑system support is important to you, consider adding an attestation path.
- You can integrate a verification-layer adapter that takes an aggregate BLS signature over your proof digests. Make state transitions dependent on either “raw verify” or “attested‑verified” and price both options. This way, power users can pick between lower latency or reduced gas fees. (blog.alignedlayer.com)
Emerging best practices we recommend in 2026
- Always opt for one multi-pairing per call. If you design something that makes multiple pairing precompile calls in a single transaction, you're looking at a gas leak of 45k for each extra call (BN254) or 37.7k for BLS12-381. Check it out here.
- If you're diving in fresh after Pectra and don’t rely on BN254, think about going for BLS12‑381 for your end-to-end cryptography. It offers stronger security margins and you’ll benefit from native MSM/mapping precompiles, plus the verification costs are pretty competitive. Check it out here: (eips.ethereum.org)
- Think of calldata as a line item in your budget. While individual proofs might be small, grouping a bunch of them together in a single transaction can really affect your zeros/non-zeros distribution under EIP-2028/EIP-7623 and ABI packing. Just remember to keep everything big-endian and uncompressed for precompile compatibility. (eips.ethereum.org)
- Make sure to keep your verifiers upgradable by utilizing a timelock or an indirection method (like Registry → Verifier). This way, you can easily switch between BN254 and BLS12‑381 verifiers or toggle between raw and attested paths without messing with your application logic. The ecosystem is evolving fast, so don’t box yourself in! (7blocklabs.com)
Reality check: when “just optimize the verifier” is enough
If you're just checking the occasional proof and can handle about 200-250k gas on L1, a hardened, single-call BN254 verifier with a small ℓ works just fine--and it's easier to manage. But if you think you’ll be verifying a bunch of proofs per block or across different chains, then you might want to consider one of these options:
- You can batch on-chain using random linear combination and end up with around 40-80k gas per proof, or
- Go for a verification layer, which will give you about 40k per proof (plus a fixed batch base) while keeping latency in the milliseconds range and maintaining Ethereum-level security. Check out more details at (fractalyze.gitbook.io).
Brief, in‑depth details: why you can’t reduce to “2 pairings” on Ethereum
Groth16’s verifier equation can actually be expressed using three pairings in a mathematical sense. However, when it comes to Solidity verifiers, they actually push four pairs into the multipairing precompile. This is mainly to dodge GT arithmetic (which isn’t available in the EVM) and to streamline everything into a single “product = 1” check. Since we’re not working with GT operations or specialized precompiles, we can’t simplify it down to just two pairings on Ethereum. That’s why you end up with those floors of 181k and 168.1k for BN254 and BLS12, respectively, when k=4. So, if you come across a claim saying “<100k per single proof on L1,” just know that it’s probably amortized, off‑chain-attested, or not even using Groth16. You can dive deeper into the details here.
If you're looking for us to set up a drop-in verifier for your stack, we'll take a close look at both the Batch‑BN254 and Attested‑BLS12 paths. We'll benchmark them against your actual circuits and public I/O, and then provide you with the precise budgets and contracts that are ready for swapping.
References:
- EIP‑1108: This one talks about how the costs for BN254 precompiles have been slashed. We're looking at pairing costs of 45,000 plus an additional 34,000 times k, with ECADD at 150 and ECMUL at 6,000. You can read more about it here.
- EIP‑2537: This introduces the BLS12‑381 precompiles that are set to roll out with Pectra on May 7, 2025. Pairing costs will be around 37,700 plus 32,600 times k, and it also includes MSM and map precompiles. Check out the details here.
- Typical Groth16 gas math on Ethereum: If you're diving into Groth16, expect gas costs to be around 207.7k plus 7.16k times ℓ. Most common verifiers use 4 pairings in a single call. Find out more here.
- Batch verification: This can cut down the number of pairings to n+2 using RLC, and the cost per proof pairing sits at roughly 34k on BN254 when scaled. More info is available here.
- Verification layer economics (Aligned): Expect about 350k base costs for each batch, with around 40k per proof when you're hitting a batch size of about 20. Plus, you’ll be looking at verification times in the millisecond range. Learn more here.
- Precompile usage and pitfalls: It’s crucial to know about generated verifier hardenings and the potential risks of input aliasing foot guns. Dive into the discussion here.
- Cross‑chain variance in precompile gas: Just a heads-up--don't hardcode STATICCALL stipends, as gas costs can vary across chains. Get the full scoop here.
Like what you're reading? Let's build together.
Get a free 30-minute consultation with our engineering team.
Related Posts
ByAUJay
Building 'Private Social Networks' with Onchain Keys
Creating Private Social Networks with Onchain Keys
ByAUJay
Tokenizing Intellectual Property for AI Models: A Simple Guide
## How to Tokenize “Intellectual Property” for AI Models ### Summary: A lot of AI teams struggle to show what their models have been trained on or what licenses they comply with. With the EU AI Act set to kick in by 2026 and new publisher standards like RSL 1.0 making things more transparent, it's becoming more crucial than ever to get this right.
ByAUJay
Creating 'Meme-Utility' Hybrids on Solana: A Simple Guide
## How to Create “Meme‑Utility” Hybrids on Solana Dive into this handy guide on how to blend Solana’s Token‑2022 extensions, Actions/Blinks, Jito bundles, and ZK compression. We’ll show you how to launch a meme coin that’s not just fun but also packs a punch with real utility, slashes distribution costs, and gets you a solid go-to-market strategy.

