7Block Labs
Technical

ByAUJay

How Do Upgrade Paths Typically Work When Moving From Single-Proof Verification to Aggregated Proofs on the Same Contract?

Short description: A practical, engineering-first playbook for upgrading an existing on-chain verifier from single-proof checks to aggregated proofs on the exact same contract address—covering ABI patterns, storage/versioning, gas math post-Pectra, precompiles you should target, and rollout tactics that won’t break production.


Executive summary for decision‑makers

  • Since March 2024’s Dencun (EIP‑4844) and May 7, 2025’s Pectra activation, Ethereum gained cheap blob DA, a KZG point‑evaluation precompile at 0x0A, and BLS12‑381 crypto precompiles (EIP‑2537). These materially change the economics of proof submission and verification, and they enable clean, additive paths from single‑proof verification to aggregated proofs without migrating addresses. (eips.ethereum.org)
  • Typical verifier upgrades adopt an additive ABI: keep your original verify(bytes, inputs) entrypoint and add verifyAggregated(...), wire new verifying keys, pin statement domains, and gate upgrades behind UUPS/Transparent proxies or Diamond facets. The practical goal is dual‑running single and aggregated verification until aggregated routes are proven and measurable. (docs.openzeppelin.com)

Why teams are upgrading now: the economics changed

  • Single Groth16 verifies typically cost ~200k–300k gas on L1 (plus ~7k gas per public input); STARK verifies often exceed 1M gas. With recursive aggregation or SNARK‑packers, batches compress to a constant‑ish verify in the hundreds of thousands of gas, amortizing per‑proof cost. (7blocklabs.com)
  • EIP‑4844 added blob DA and a KZG point‑evaluation precompile at 0x0A with a fixed 50,000 gas cost per opening, letting you bind proofs to blob content cheaply and keep calldata small (important under EIP‑7623’s calldata floor). (eips.ethereum.org)
  • Pectra (May 7, 2025; epoch 364032) shipped BLS12‑381 precompiles (EIP‑2537). For pairings, the on‑chain cost is 32,600·k + 37,700 gas, with additional cheap MSM ops—useful for BLS signature aggregation flows or BLS‑based SNARK verifiers. (blog.ethereum.org)

What “aggregation” buys you in practice:

  • One on‑chain verify per batch rather than N single verifies.
  • Typical numbers cited in production systems: ~300k–350k gas per aggregated verify plus ~7k gas per proof for bookkeeping; or ~275–300k gas for a wrapped zkVM proof. (docs.nebra.one)
  • Off‑chain options like Aligned’s Verification Layer use restaked operators to verify many proofs off‑chain and post aggregate BLS attestations on L1—~350k gas for a batch of one, ≈40k per proof at batch size 20. These can be part of a hybrid rollout (fast path + L1 aggregation for finality). (blog.alignedlayer.com)

Design goal: upgrade on the same address, keep the original API working

Most teams don’t want to rotate the verifier address. Two robust paths:

  1. Upgradeable proxy with an additive ABI
  • Keep verify(bytes proof, uint256[] publicInputs) unchanged.
  • Add verifyAggregated(...) and optionally a read‑only view that exposes the current verifying key hash and circuit version.
  • Use UUPS or Transparent proxies; run storage‑layout checks (OpenZeppelin plugins) before upgrades. (docs.openzeppelin.com)
  1. Diamond (ERC‑2535) facets
  • If your verifier is near the 24KB EIP‑170 size limit, move verifier functions into a “VerifierFacet” and aggregate logic into an “AggregatorFacet”; cut in via diamondCut without changing the address. This also lets you ship multiple curves/circuits as separate facets. (eips.ethereum.org)

Note on size limits: mainnet still enforces 24KB per contract (EIP‑170). Draft EIPs discuss increases, but you should plan within 24KB or modularize. (eips.ethereum.org)


ABI patterns we recommend (additive and versioned)

Expose these minimal, additive entrypoints:

// Original single-proof entrypoint (unchanged)
function verify(bytes calldata proof, uint256[] calldata pubInputs)
  external view returns (bool ok);

// New: aggregated batch verification
function verifyAggregated(
  bytes calldata aggProof,        // the recursive/aggregated proof
  bytes32 aggVkHash,              // verifying key hash for the aggregator circuit
  bytes32 batchId,                // opaque batch identifier
  bytes32 batchStatementHash      // domain-separated hash of all included statements
) external view returns (bool ok);

// Optional: per-proof inclusion checks against the stored batch root
function isIncluded(bytes32 batchId, bytes32 statementLeaf, bytes32[] calldata merkleProof)
  external view returns (bool);

// Introspection: help off-chain ops pin exact VKs / circuit versions
function verifierConfig() external view returns (
  bytes32 singleVkHash,
  bytes32 aggVkHash,
  uint64 singleCircuitVersion,
  uint64 aggCircuitVersion
);
  • Keep verify(...) stable for existing integrators.
  • Don’t pass large per‑proof metadata in calldata under EIP‑7623; instead, commit to a Merkle root in batchStatementHash and verify membership ad‑hoc as needed (cheaper and flexible). (eips.ethereum.org)

Verification key handling: store, pin, and roll safely

Your upgrade will introduce new verifying keys (VKs) for the aggregator circuit. Practical patterns:

  • Pin VKs by content hash in contract storage, and require aggVkHash to match. It removes ambiguity when multiple VKs coexist.
  • Store bulky VK bytes with SSTORE2 pointers and keep the pointer and its keccak256 in storage. Reads use EXTCODECOPY, which is cheaper than SLOAD for big blobs; updates are “write‑once” by design. (github.com)
  • Version your circuits explicitly. A simple uint64 version plus the VK hash is enough for auditability and rollbacks.
  • Emit VerifierUpgraded(vkHash, circuitVersion) and VerifierKeyRotated(oldHash, newHash) events.

Tip: where a statement references chain data, include the block number and chain-id in your statement hash. With EIP‑2935 and EIP‑4788 you can verify L1 blockhashes and beacon roots on-chain without external oracles, strengthening your domain separation. (eips.ethereum.org)


Curve and precompile choices: BN254 vs BLS12‑381 post‑Pectra

  • Legacy verifiers on Ethereum often target BN254 (alt_bn128) because of long‑standing precompiles and EIP‑1108 gas reductions for ECADD/ECMUL/pairing (pairings ~34,000·k + 45,000 gas). If your single‑proof verifier is BN254 Groth16, you can keep it. (eips.ethereum.org)
  • For new aggregated circuits or BLS aggregation, BLS12‑381 (EIP‑2537) is now first‑class on mainnet with explicit MSM and pairing precompiles. Pairings cost 32,600·k + 37,700 gas; G1/G2 adds are 375/600 gas; FP/FP2 map are 5,500/23,800 gas. Verify your aggregator’s curve matches precompile availability on your deployment chain(s). (eips.ethereum.org)
  • Mixed ecosystems: Some L2s are progressively adopting Pectra equivalents; check your target chain. The OP Stack tracked BLS12‑381 support tied to its Pectra/Isthmus alignment—don’t assume every chain has EIP‑2537 live yet. (github.com)

Keep calldata small: blobs + point‑evaluation precompile

If your aggregated proof needs to reference many leaf statements, don’t dump them into calldata (EIP‑7623 raises costs for data‑heavy txs). Instead:

  • Publish the per‑proof metadata into blobs (EIP‑4844).
  • In your contract, verify small field‑element bindings via the 0x0A point‑evaluation precompile (50k gas) to assert you’re anchored to the right blob data, and verify your one succinct aggregated proof. (eips.ethereum.org)

Result: calldata stays small, you avoid worst‑case floor pricing, and your batch still inherits L1 security while consuming blobspace (whose capacity doubled to a 6/9 target/max per block in Pectra via EIP‑7691). (eips.ethereum.org)


Memory/copy costs matter in verifiers: target MCOPY

Dencun added EIP‑5656 (MCOPY). Solidity 0.8.25’s codegen uses mcopy() automatically, cutting the gas for memory copies in proof parsing and MSM inputs. When you migrate to aggregated verification (larger calldata sections for the single call), compile with >=0.8.25 and EVM version “cancun” to realize the savings. (soliditylang.org)


Three aggregation paths we see in production

  1. Recursive SNARK wrapping (e.g., STARK/FRI → Groth16/Plonk)
  • Leaf proofs get verified inside a recursion circuit and wrapped into a succinct proof. On-chain verify becomes O(1) pairings with small public inputs. Batches of thousands often come in at ≈300k–900k gas once per batch. (7blocklabs.com)
  1. Aggregation schemes like SnarkPack/aPlonk
  • Aggregate many existing Groth16/Plonk proofs without redesigning leaf circuits; SnarkPack reported aggregating 8192 Groth16 proofs in ~8–9s with sub‑200ms verification off‑chain. On‑chain, you verify a succinct aggregator proof. (eprint.iacr.org)
  1. External aggregation/verification layers
  • Examples include Aligned (AVS on EigenLayer) and universal proof aggregators (e.g., NEBRA UPA, Electron). You submit or register proofs off‑chain; they return on‑chain with a single aggregated proof or a BLS‑signed result. Gas today: ~300k–380k fixed per batch plus small per‑proof costs; per‑proof gas drops as batch size grows. (blog.alignedlayer.com)

Which to use?

  • If you own the circuits and throughput is high, recursion/wrapping eliminates per‑proof gas; great for rollups.
  • If you have many heterogeneous circuits or can’t re‑engineer leaves, SnarkPack‑style aggregation is pragmatic.
  • If you want faster time‑to‑market and can accept an AVS or external protocol dependency, integrate an aggregation/verification service, keeping a fallback to direct L1 verify for hard finality. (blog.alignedlayer.com)

A concrete upgrade plan that keeps your address

Phase 0 — Prep (1–2 sprints)

  • Freeze current verifier ABI (verify stays stable).
  • Generate aggregator circuit and keys; record aggVkHash; plan storage layout extension; write migration tests.
  • Decide proxy pattern: UUPS (overrides _authorizeUpgrade) or Diamond facet with VerifierFacet/AggregatorFacet split. (docs.openzeppelin.com)

Phase 1 — Additive deployment (1 sprint)

  • Ship a new implementation adding:
    • verifyAggregated(...) (view) that checks aggVkHash and enforces a domain‑separated batchStatementHash.
    • verifierConfig() exposing hashes/versions.
    • Optional isIncluded(...) with a Merkle verifier.
  • Store large VKs with SSTORE2 pointers; pin hashes in storage. (github.com)

Phase 2 — Shadow mode (2–4 sprints)

  • Off‑chain aggregator runs, but on‑chain consumers still call single verify(...).
  • You periodically submit aggregated verifies to exercise the path and log batchIds; monitor gas and latency.
  • If statements are large, ship blob flows + 0x0A point‑evaluation bindings now; verify that calldata stays below EIP‑7623 floor thresholds under your workloads. (eips.ethereum.org)

Phase 3 — Controlled traffic shift

  • Introduce a routing flag in your application contracts to prefer aggregated verification when batch availability SLOs are met; otherwise fall back to single verify.
  • Set SLOs by chain:
    • Ethereum L1: budget ~300k–350k gas per aggregated verify; keep per‑proof inclusion costs out of hot paths (log events, Merkle membership on demand). (docs.nebra.one)
    • Pectra‑aligned chains: leverage BLS12‑381 precompiles for any BLS attestation or BLS‑based SNARKs in your stack. (blog.ethereum.org)

Phase 4 — Hardening

  • Add timelocks and a kill‑switch to force the single‑proof path if aggregator liveness dips.
  • Lock VK hashes after a cooling period; require governance to rotate.
  • Ship dashboards: success rates, batch sizes, blob fee tracking (EIP‑7691 changes blob dynamics), and calldata/MCOPY hit‑rates. (eips.ethereum.org)

Gas and capacity planning you can take to a budgeting meeting

  • Single Groth16 verify: ≈200k–300k gas + ~7k gas per public input. Rule‑of‑thumb model ≈ 207,700 + 7,160·l, l = public signals. Keep l tiny by hashing bulky statements. (7blocklabs.com)
  • Aggregated verify via Halo2‑KZG (reported): ~350k base per batch + ≈7k gas/proof for bookkeeping. With N=32, ≈18k gas per proof amortized. (docs.nebra.one)
  • KZG binding to blobs: 50k gas per point evaluation at 0x0A; budget one or a few openings per batch. (eips.ethereum.org)
  • BLS12‑381 pairing math: 32,600·k + 37,700 gas for k pairings; use MSM precompile for big multi‑signer checks instead of looping MUL/ADD. (eips.ethereum.org)

Capacity implication: at 45M gas blocks, you’ll rarely fit thousands of single verifies, but one aggregated verify plus inclusion bookkeeping (or even multiple batches) is comfortable—this is why aggregation materially cuts end‑to‑end latency when the bottleneck is verification inclusion, not finality. (7blocklabs.com)


Security and operational pitfalls (and how to avoid them)

  • Domain separation: Define batchStatementHash as keccak256(“agg:v1” || chainId || l1Block || vkHash || MerkleRoot || …). Don’t reuse the single‑proof message domain.
  • VK pinning: Require aggVkHash to match storage and emit events on rotation; never accept VK bytes from calldata without matching the expected hash.
  • Liveness fallback: A stuck aggregator must not halt your protocol. Keep single verify callable and rate‑limited for safety.
  • Gas griefing: If you mark inclusion with SSTORE per proof, attackers can force you into 20k‑gas writes. Prefer logs for auditability and on‑demand Merkle membership checks over “mark and store” for each leaf.
  • Cross‑chain differences: Not all L2s enable EIP‑2537 yet; feature‑detect precompiles and ship a BN254 verifier facet as a fallback. Track OP Stack and other rollup stacks’ Pectra support. (github.com)
  • Code size creep: If adding aggregation pushes you near 24KB, move proof parsing or VK bytes behind SSTORE2 or split into Diamond facets. (eips.ethereum.org)

Example: additive migration diff (Solidity pseudocode)

contract Verifier is UUPSUpgradeable {
  // v1 fields (unchanged)
  bytes32 public singleVkHash;
  uint64  public singleCircuitVersion;

  // v2 fields (new)
  bytes32 public aggVkHash;            // hash of Aggregator VK
  uint64  public aggCircuitVersion;
  address public aggVkPointer;         // SSTORE2 pointer with VK bytes

  event VerifierUpgraded(bytes32 singleVkHash, uint64 singleVer, bytes32 aggVkHash, uint64 aggVer);
  event BatchVerified(bytes32 batchId, bytes32 batchStatementHash);

  function verify(bytes calldata proof, uint256[] calldata pubInputs) external view returns (bool) {
    // existing single-proof verification (unchanged)
  }

  function verifyAggregated(
    bytes calldata aggProof,
    bytes32 aggVkHash_,
    bytes32 batchId,
    bytes32 batchStatementHash
  ) external view returns (bool ok) {
    require(aggVkHash_ == aggVkHash, "VK_MISMATCH");
    // 1) Optionally call 0x0A to bind to blob content for this batch
    // 2) Run aggregator verifier using BLS12-381 precompiles (EIP-2537) or BN254, depending on your circuit
    // 3) Domain-separate and return true on success
  }

  function isIncluded(bytes32 batchId, bytes32 leaf, bytes32[] calldata merkleProof)
    external view returns (bool) {
    // verify membership against the batch root embedded in batchStatementHash
  }

  function _authorizeUpgrade(address newImpl) internal override onlyOwner {}
}
  • Keep the old verify() intact to preserve integrator contracts.
  • Batches log BatchVerified so downstreams can consume off‑chain and only call isIncluded when needed.
  • If your aggregator circuit is BLS12‑381‑based, map to EIP‑2537 precompiles for MSM + pairings; if not available on a target L2, route to a BN254 fallback. (eips.ethereum.org)

“Gotchas” born from new protocol rules

  • EIP‑7623’s calldata repricing means large calldata‑heavy aggregated submissions can be penalized; blobs + 0x0A fix this. If you must pass data via calldata, keep a healthy ratio of execution gas to calldata to avoid the higher floor. (eips.ethereum.org)
  • Pectra increased blob throughput (target/max 6/9), which generally keeps blob fees low, but you still need monitoring—don’t assume perpetual cheapness. (eips.ethereum.org)
  • MCOPY only helps if you compile with the right Solidity/EVM settings; verify your optimizer and evmVersion in CI. (soliditylang.org)

Benchmarks and references you can cite internally

  • SnarkPack (Groth16): 8192 proofs aggregated in ≈8–9s on 32‑core CPU; verification ≈33–163 ms off‑chain; on‑chain verify remains a succinct pairing check. (eprint.iacr.org)
  • NEBRA UPA (Halo2‑KZG aggregation): ~350k gas base verify + ~7k per included proof; ≈18k gas/proof with N=32. (docs.nebra.one)
  • Succinct SP1 wrap: ~275–300k gas per compressed proof on EVM; production deployments increasingly use this to amortize zkVM workloads. (7blocklabs.com)
  • Aligned Verification Layer (AVS): ~350k gas per batch of one, ≈40k per proof at batch size 20; milliseconds latency; hard aggregation path also offered. (blog.alignedlayer.com)

Upgrade checklist (use this with your PM and auditor)

  • Governance

    • Timelocked UUPS/Transparent proxy or Diamond with restricted diamondCut. (docs.openzeppelin.com)
    • Multisig + clear Runbook to revert to single‑proof verify.
  • Storage and ABI

    • New fields appended; storage layout diff passes (OpenZeppelin plugins). (github.com)
    • SSTORE2 pointers for big VKs; VK hashes pinned. (github.com)
    • verify() unchanged; verifyAggregated() + verifierConfig() added.
  • Cryptography

    • Curve and precompile availability audited per chain (BN254 vs BLS12‑381). (eips.ethereum.org)
    • Domain separation includes chainId, block context (EIP‑2935/4788 where relevant). (eips.ethereum.org)
  • Data plumbing

    • Blob‑first for bulky metadata; bind with 0x0A point‑evaluation openings in‑contract. (eips.ethereum.org)
    • Calldata footprint measured under EIP‑7623 thresholds. (eips.ethereum.org)
    • Solidity >=0.8.25 to benefit from MCOPY in proof parsing. (soliditylang.org)
  • Rollout

    • Shadow mode: aggregated path exercised in production without changing consumers.
    • SLOs: batch size, latency, and failure fallback defined; dashboards include blob fee health.

Bottom line

Moving from single‑proof verification to aggregated proofs on the same contract is a low‑risk, high‑ROI upgrade in 2026. Do it additively: keep your existing ABI running, append an aggregated entrypoint, pin VKs and domains, store big artifacts off‑storage, and route bulky metadata to blobs verified via 0x0A. Pectra’s BLS12‑381 precompiles and the calldata repricing make these design choices both cheaper and safer than in 2023–2024. With a dual‑run rollout and clear fallbacks, most teams realize immediate gas savings, fewer on‑chain transactions, and better throughput without sacrificing their address or integrator compatibility. (blog.ethereum.org)


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

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

Related Posts

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.

© 2025 7BlockLabs. All rights reserved.