7Block Labs
Technical

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:

  1. 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)
  2. 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.

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.