ByAUJay
From EIP-2537 to Production: Verifying BLS Signatures in Solidity Without Tears
Short version: Ethereum’s May 7, 2025 Pectra upgrade added native BLS12-381 precompiles (EIP-2537), finally making on-chain BLS verification practical on mainnet. Below is a concrete, production-focused guide to shipping BLS-based signatures and aggregates in Solidity on Ethereum (and Pectra-aligned L2s), with specific byte layouts, gas math, and code patterns you can drop into audits. (eips.ethereum.org)
Who should read this
- Startup and enterprise leaders evaluating BLS-backed wallets, cross-chain bridges, light clients, or validator tooling.
- Engineering managers who need precise implementation details, not hand-wavy “BLS is fast now.”
What changed in 2025 (and why it matters)
- Pectra (Prague × Electra) went live on Ethereum mainnet on May 7, 2025 (epoch 364032), and it explicitly includes EIP-2537. That means your Solidity can now call BLS12-381 precompiles at fixed addresses without resorting to unsafe big-integer libraries or L2-only tricks. Testnet activation blocks/epochs are also fixed in the meta-EIP. (eips.ethereum.org)
- Cancun (March 2024) had already added the KZG point-evaluation precompile at 0x0A via EIP-4844; Pectra completes the story by adding general BLS12-381 ops for signatures and pairings. Together, these let you prove blob data consistency (KZG) and authenticate with BLS signatures in a single on-chain flow. (eips.ethereum.org)
Why you should care:
- On-chain verification of validator-style aggregate signatures is now feasible in Solidity with predictable gas. This unlocks design space in bridges, DA attestors, rollup sequencer committees, and multi-sig/threshold wallets where BLS’s aggregation properties materially cut costs and latency. (eips.ethereum.org)
The new precompiles you can actually call
EIP-2537 installs seven precompiles at these addresses:
- 0x0b: BLS12_G1ADD
- 0x0c: BLS12_G1MSM (multi-scalar multiplication)
- 0x0d: BLS12_G2ADD
- 0x0e: BLS12_G2MSM
- 0x0f: BLS12_PAIRING_CHECK
- 0x10: BLS12_MAP_FP_TO_G1
- 0x11: BLS12_MAP_FP2_TO_G2 (eips.ethereum.org)
Key details you’ll actually need:
- Pairing check gas: 37700 + 32600 × k, where k is the number of (G1, G2) pairs you pass. For one basic verify (2 pairings), budget ≈ 102,900 gas purely for the pairing. Plan capacity accordingly. (eips.ethereum.org)
- Encodings are big-endian and uncompressed:
- Fp element: 64 bytes (top 16 bytes MUST be zero because p is 381 bits).
- G1 point: 128 bytes = x(64) || y(64).
- Fp2 element: 128 bytes = c0(64) || c1(64).
- G2 point: 256 bytes = x(128) || y(128).
- Infinity is all zeros for the point size. (eips.ethereum.org)
- Subgroup checks:
- MSM and pairing MUST perform subgroup checks.
- G1ADD and G2ADD do NOT do subgroup checks. Validate inputs or only add points from trusted sources. (eips.ethereum.org)
- Mapping precompiles:
- 0x10/0x11 map field elements (Fp / Fp2) into curve points. They do NOT hash bytes to field elements; you must run hash_to_field yourself (e.g., IETF RFC 9380) before calling MAP. (eips.ethereum.org)
For context, EIP-4844’s KZG point-evaluation precompile sits at 0x0A and uses a fixed 192-byte ABI; you’ll often call it before your BLS verify when the message you’re verifying lives in a blob. (eips.ethereum.org)
BLS on Ethereum: what the beacon chain taught us
- Ethereum’s consensus uses BLS12-381 with public keys in G1 (48 bytes compressed) and signatures in G2 (96 bytes compressed), matching the IETF BLS ciphersuites and Eth2 specs. For on-chain verification, you use uncompressed points per EIP-2537. (docs.radixdlt.com)
- Aggregation patterns:
- Basic verify: e(pk, H(m)) == e(G1, σ)
- Fast aggregate verify (same message, many signers): aggregate pk = ∑ pk_i (in G1), then e(agg_pk, H(m)) == e(G1, σ)
- Aggregate verify (distinct messages): e(pk1, H(m1)) * … * e(pkn, H(mn)) == e(G1, σ)
You feed these as one pairing_check on k+1 pairs by negating one operand so the product equals 1 in the target field. (notes.ethereum.org)
Byte layouts you must get right
- Big-endian everywhere for EIP-2537 inputs. Solidity’s usual ABI types are little-endian when treated as integers in memory; do not reinterpret cast; build the exact byte sequence with abi.encodePacked. (eips.ethereum.org)
- Supply uncompressed points directly; decompression on-chain is more gas than sending decompressed coordinates in calldata. This is explicitly noted in the spec and worth real money at scale. (eips.ethereum.org)
- Infinity is encoded as all-zero bytes for the point size. Use that for guard rails in your decoders. (eips.ethereum.org)
Minimal Solidity: calling the pairing precompile
Goal: verify a single signature in the “pubkey in G1, signature in G2” scheme against hash_to_G2(m).
- Equation: e(pk, H) == e(G1, σ)
- Pairing check expects product equals 1, so we pass pairs (pk, H) and (−G1, σ).
We avoid big-number gymnastics in this snippet by requiring the caller to precompute −G1 once (constant) or to provide a negated σ. In production, we recommend precomputing −G1 and wiring it as an immutable constant.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; // 7Block Labs: minimalistic BLS12-381 pairing check via EIP-2537 (Pectra) // Inputs must be uncompressed big-endian encodings per EIP-2537. // - pkG1: 128 bytes (x||y) // - HmG2: 256 bytes (x_c0||x_c1||y_c0||y_c1) – this is hash_to_G2(m), see notes below // - sigG2: 256 bytes // - negG1: 128 bytes encoding of -G1 generator (precomputed once and reused) library BLS12381Verify { address constant PAIRING = address(0x0f); // BLS12_PAIRING_CHECK function verifyBasic( bytes memory pkG1, bytes memory HmG2, bytes memory sigG2, bytes memory negG1 ) internal view returns (bool ok) { require(pkG1.length == 128 && HmG2.length == 256 && sigG2.length == 256 && negG1.length == 128, "bad-len"); // Build input to pairing precompile: concat of k slices, each 128 (G1) + 256 (G2) = 384 bytes. bytes memory input = new bytes(384 * 2); // slice 1: (pkG1, HmG2) assembly { let ptr := add(input, 32) // copy pkG1 calldatacopy(ptr, add(pkG1, 32), 128) // copy HmG2 calldatacopy(add(ptr, 128), add(HmG2, 32), 256) // slice 2: (-G1, sigG2) calldatacopy(add(ptr, 384), add(negG1, 32), 128) calldatacopy(add(ptr, 512), add(sigG2, 32), 256) } // Output is 32 bytes: last byte 0x01 if true, else 0x00. bytes memory out = new bytes(32); assembly { if iszero(staticcall(gas(), PAIRING, add(input, 32), mload(input), add(out, 32), 32)) { revert(0, 0) } ok := eq(byte(31, mload(add(out, 32))), 1) } } }
Notes:
- negG1 is the G1 generator with y negated modulo p. Precompute it off-chain once using any mature BLS12-381 library and hard-code it; it never changes. Ethereum’s fixed generators are standardized; you can derive −G1 from the published G1 generator coordinates. (eth2book.info)
- If you prefer to negate the signature instead, pass (pk, H) and (G1, −σ). Negation in G2 flips the y coordinate modulo p for both Fp components; do this off-chain to avoid heavy big-int code in Solidity.
- The pairing precompile already enforces subgroup membership for these inputs; do not rely on that for points you later add with G1ADD/G2ADD, which skip subgroup checks. (eips.ethereum.org)
Getting H = hash_to_G2(m) right
EIP-2537 exposes map_fp2_to_g2 (0x11), but “hash bytes → field elements” (expand_message_xmd, etc.) is on you. The IETF’s RFC 9380 defines how to hash to BLS12-381 G2 (e.g., BLS12381G2_XMD:SHA-256_SSWU_RO_), including cofactor clearing. Typical production architectures do this off-chain and submit the resulting uncompressed G2 point to the contract. If you must do it on-chain, wire SHA-256 precompile and implement expand_message_xmd, then call MAP_FP2_TO_G2 for the map step. (ietf.org)
Practical advice:
- For wallets/bridges you control end-to-end, mandate the exact ciphersuite in your spec and reject anything else.
- For open systems, require the submitter to provide H as a point and a “hash-to-curve proof” (SNARK) if you can’t trust their hashing path. You then only verify the proof + pairing, keeping gas bounded and trust minimized.
Fast-aggregate verify with on-chain aggregation
When many signers sign the same message m:
- Compute agg_pk = ∑ pk_i. Use G1MSM (0x0c) with scalars all set to 1 for best gas; it’s faster than repeated ECADD because it runs Pippenger’s algorithm and amortizes call overhead. Then verify e(agg_pk, H(m)) == e(G1, σ). (eips.ethereum.org)
Sketch:
// Pseudocode for using G1MSM to aggregate N pubkeys: // Input to 0x0c is k slices of (G1 point 128 bytes || scalar 32 bytes). // Here scalar is 1 for each point. // Output is a single 128-byte G1 point. function aggregatePks(bytes[] memory pksG1) internal view returns (bytes memory aggPk128) { uint256 n = pksG1.length; require(n > 0, "no-pks"); bytes memory input = new bytes(160 * n); // 128 + 32 per entry for (uint256 i = 0; i < n; i++) { require(pksG1[i].length == 128, "bad-pk"); // copy pk // write scalar=1 as big-endian 32 bytes // ... build input ... } bytes memory out = new bytes(128); assembly { if iszero(staticcall(gas(), 0x0c, add(input, 32), mload(input), add(out, 32), 128)) { revert(0, 0) } } aggPk128 = out; }
Gas planning:
- Aggregation via MSM scales sublinearly (discount function within the gas schedule). Pairing then costs ≈ 37700 + 2×32600 for the final verify (two pairs). For k signers, the total is MSM(input size dependent) + ~102,900 pairing gas. Profile your exact k on Holesky or a local fork; the precompile’s discount is input-length aware. (eips.ethereum.org)
Safety tips:
- Only add/subgroup-check-validated keys. MSM performs subgroup checks; ECADD does not. Treat raw user-supplied points as hostile unless proven otherwise. (eips.ethereum.org)
Aggregate verify for distinct messages
For (pk_i, m_i) i=1..n and single aggregate signature σ:
- Build one pairing input with n+1 slices: (pk_1, H(m_1)), …, (pk_n, H(m_n)), (−G1, σ).
- Gas is roughly 37700 + 32600×(n+1). Ten messages? ≈ 37700 + 11×32600 ≈ 395,300 gas for the pairing, plus your hash_to_curve cost and calldata. This is finally practical for many bridge and committee designs. (eips.ethereum.org)
Interop with EIP-4844 (KZG) in real applications
A common pattern in rollups and DA attestations:
- Use the 0x0A KZG point-evaluation precompile to assert that a blob’s polynomial evaluates to y at z and matches a versioned hash (EIP-4844). (eips.ethereum.org)
- Verify that the entity attesting to that blob evaluation (sequencer, committee) signed it with BLS using EIP-2537 pairing.
This gives you L1-anchored data integrity (KZG) and authenticated attestations (BLS) with native precompiles end-to-end.
Exact encodings you’ll pass around (copy-paste friendly)
- Fp modulus (p) for BLS12-381:
p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
EIP-2537 requires every Fp encoding to be 64-byte big-endian with the top 16 bytes zero. (eips.ethereum.org) - G1/G2 point encodings are uncompressed (x||y) with x and y each built from these 64-byte Fp (or Fp2) encodings. Infinity is all zeros. (eips.ethereum.org)
- Pairing precompile output: a 32-byte blob where the last byte is 0x01 for “true” and 0x00 for “false.” (eips.ethereum.org)
Best emerging practices we see in audits
- Enforce a single BLS ciphersuite across your stack (e.g., BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_) and domain separation tags; reject others to avoid downgrade ambiguity. (ietf.org)
- Keep points uncompressed in calldata to avoid on-chain decompression costs (it really is more expensive). If your front-end or off-chain agent receives compressed inputs (48/96 bytes), decompress off-chain using blst/herumi and submit 128/256 bytes to contracts. (eips.ethereum.org)
- Treat addition precompiles as “unsafe” for untrusted inputs; prefer MSM or pairing (they enforce subgroup checks). (eips.ethereum.org)
- Precompute and hard-code constants like G1, −G1, and byte-aligned modulus fragments. It reduces complexity and audit surface.
- For throughput: batch verifications into a single pairing_check. Gas grows linearly in pair count with a modest constant; it’s cheaper than many tiny calls. (eips.ethereum.org)
- If your messages are derived from blobs, always verify KZG (0x0A) before trusting signatures that reference blob content. (eips.ethereum.org)
- Watch EVM equivalence on your target chain. Most major rollups track mainnet precompiles, but verify that 0x0b–0x11 are live on your L2/testnet before deployment; use Pectra activation info from EIP-7600 to automate environment checks in CI. (eips.ethereum.org)
Production checklist (copy to your runbook)
- Network readiness
- Pectra (EIP-7600) activated on target chain (mainnet, L2, or testnet); verify addresses 0x0b–0x11 respond. (eips.ethereum.org)
- If consuming blobs, confirm 0x0A point-evaluation precompile works. (eips.ethereum.org)
- Encoding
- All Fp/Fp2 elements are 64/128 bytes big-endian with top 16 bytes zero for each Fp element.
- All G1/G2 points are uncompressed encodings (128/256 bytes).
- Infinity represented as all zeros for the point size. (eips.ethereum.org)
- Security
- Subgroup checks are enforced by using MSM/pairing for any untrusted points.
- Where ECADD is used, ensure points are validated beforehand.
- Hash-to-curve follows RFC 9380; domain separation string pinned; map step done via 0x10/0x11 or entirely off-chain. (ietf.org)
- Gas & UX
- Batch verifications into single pairing_check calls to amortize the 37.7k base cost. (eips.ethereum.org)
- Measure MSM vs repeated ECADD for your key counts; prefer MSM when aggregating many keys. (eips.ethereum.org)
Example: contract-level API for BLS aggregator
A realistic surface for an ERC-4337-style aggregator or bridge verifier:
interface IBLSVerifier { // Returns true if e( sum(pk_i), H(m) ) == e(G1, sigma ) // Requires caller to provide: // - pkList: array of uncompressed G1 pubkeys (each 128 bytes) // - HmG2: uncompressed hash_to_G2(m) (256 bytes) // - sigmaG2: uncompressed aggregate signature (256 bytes) // - negG1: cached 128-byte encoding of -G1 function fastAggregateVerify( bytes[] calldata pkList, bytes calldata HmG2, bytes calldata sigmaG2, bytes calldata negG1 ) external view returns (bool); }
Implement fastAggregateVerify with:
- G1MSM( pk_i, 1 ) → agg_pk
- Pairing on (agg_pk, HmG2) and (negG1, sigmaG2)
This pattern minimizes calldata and calls while preserving verifiability in a single transaction.
FAQs you’ll get from your team
- Does Ethereum expose compressed-point decoding in precompiles?
No. EIP-2537 uses uncompressed 128/256-byte encodings for G1/G2. Send them as such. (eips.ethereum.org) - Do I need to worry about endianness?
Yes. EIP-2537 wants big-endian field elements. Always build your calldata via abi.encodePacked, not by casting uints. (eips.ethereum.org) - Can I rely on pairing for subgroup checks?
Yes (pairing and MSM enforce this), but ECADD does not. Design accordingly. (eips.ethereum.org) - What about KZG and blobs?
Use 0x0A (EIP-4844) first to validate blob claims, then verify BLS signatures on those claims. (eips.ethereum.org)
Final thought
Before Pectra, BLS in Solidity was a research project. After May 7, 2025, it’s a product feature you can ship on Ethereum mainnet. If you standardize encodings, centralize constants like −G1, and funnel all untrusted inputs through MSM/pairing precompiles, you’ll get predictable gas, clean audits, and a straightforward path from design doc to production. (eips.ethereum.org)
References (specs we linked inline):
- EIP-7600: Pectra meta EIP with activation epochs and included EIPs. (eips.ethereum.org)
- EIP-2537: BLS12-381 precompiles, addresses, encodings, gas formulas. (eips.ethereum.org)
- EIP-4844: KZG point-evaluation precompile at 0x0A. (eips.ethereum.org)
- RFC 9380: Hashing to Elliptic Curves (choose a BLS ciphersuite and stick to it). (ietf.org)
- Eth research/docs on G1/G2 generator choices, key/signature sizes, and aggregation semantics. (ethresear.ch)
Description: Ethereum’s May 7, 2025 Pectra upgrade added native BLS12-381 precompiles (EIP-2537). This guide shows exactly how to verify single and aggregate BLS signatures in Solidity—addresses, encodings, gas math, and production-safe code patterns. (eips.ethereum.org)
Like what you're reading? Let's build together.
Get a free 30‑minute consultation with our engineering team.

