Skip to main content

BLS12-381

Pairing-friendly elliptic curve implementation for Ethereum 2.0 consensus layer signatures and EIP-2537 precompiled contracts.

Overview

BLS12-381 is a Barreto-Lynn-Scott pairing-friendly curve designed for optimal security and performance in blockchain applications. It provides 128-bit security, efficient pairing operations, and signature aggregation capabilities essential for proof-of-stake consensus. Ethereum Use Cases:
  • Ethereum 2.0 Consensus: Validator signature aggregation
  • BLS Signatures: Short signatures with efficient batch verification
  • EIP-2537: Precompiled contracts for curve operations
  • Light clients: Compact sync committee proofs
  • Cross-chain bridges: Trustless interoperability proofs
Security Level: 128-bit (comparable to 3072-bit RSA or 256-bit ECC)

Quick Start

import * as BLS12381 from '@tevm/voltaire/crypto';

// G1 operations (signatures)
const g1Point1 = new Uint8Array(128); // G1 point input
const g1Point2 = new Uint8Array(128);
const g1Output = new Uint8Array(128);
await BLS12381.bls12_381.g1Add([...g1Point1, ...g1Point2], g1Output);

// G2 operations (public keys)
const g2Point1 = new Uint8Array(256);
const g2Scalar = Bytes32();
const g2Output = new Uint8Array(256);
await BLS12381.bls12_381.g2Mul([...g2Point1, ...g2Scalar], g2Output);

// Pairing check (signature verification)
const g1_128 = new Uint8Array(128);
const g2_256 = new Uint8Array(256);
const pairingInput = new Uint8Array([...g1_128, ...g2_256]); // 384 bytes per pair
const pairingOutput = Bytes32();
await BLS12381.bls12_381.pairing(pairingInput, pairingOutput);

Elliptic Curve Pairing Basics

BLS12-381 is a Barreto-Lynn-Scott curve with embedding degree 12, providing:
  1. Efficient Pairings: Optimal ate pairing computable in ~1-2ms
  2. Signature Aggregation: Combine multiple signatures into one
  3. Batch Verification: Verify many signatures in one pairing check
  4. Short Signatures: G1 signatures (48 bytes) with G2 public keys (96 bytes)
Pairing Map: e: G1 × G2 → GT where:
  • G1: Points over base field Fp (48-byte compressed, 96-byte uncompressed)
  • G2: Points over Fp2 extension (96-byte compressed, 192-byte uncompressed)
  • GT: Elements in Fp12 (multiplicative group)
Properties:
  • Bilinearity: e(aP, bQ) = e(P, Q)^(ab)
  • Non-degeneracy: e(G1, G2) ≠ 1
  • Computability: Polynomial time optimal ate pairing

API Reference

G1 Operations

G1 points are in the base field Fp (381-bit prime).

G1 Addition

import { bls12_381 } from '@tevm/voltaire/crypto';

// Add two G1 points
const input = new Uint8Array(256); // p1 (128 bytes) || p2 (128 bytes)
const output = new Uint8Array(128);

// Each G1 point: 128 bytes
// - x coordinate: 64 bytes (Fp, padded big-endian)
// - y coordinate: 64 bytes (Fp, padded big-endian)

await bls12_381.g1Add(input, output);
Input Format: 256 bytes
  • Bytes 0-63: p1.x (Fp, padded to 64 bytes)
  • Bytes 64-127: p1.y (Fp)
  • Bytes 128-191: p2.x (Fp)
  • Bytes 192-255: p2.y (Fp)
Output Format: 128 bytes (result point)

G1 Scalar Multiplication

// Multiply G1 point by scalar
const input = new Uint8Array(160); // point (128) || scalar (32)
const output = new Uint8Array(128);

// Point: 128 bytes (x || y, each 64 bytes padded)
// Scalar: 32 bytes (Fr element, big-endian)

await bls12_381.g1Mul(input, output);
Input Format: 160 bytes
  • Bytes 0-127: G1 point (x || y)
  • Bytes 128-159: Scalar (32-byte big-endian)

G1 Multi-Scalar Multiplication (MSM)

// Multi-scalar multiplication: sum(scalar_i * point_i)
const numPoints = 10;
const input = new Uint8Array(160 * numPoints);
const output = new Uint8Array(128);

// Input: concatenated (point || scalar) pairs
await bls12_381.g1Msm(input, output);
Use case: Efficient batch operations (validators, proof aggregation)

G2 Operations

G2 points are over Fp2 extension field (complex numbers over Fp).

G2 Addition

// Add two G2 points
const input = new Uint8Array(512); // p1 (256) || p2 (256)
const output = new Uint8Array(256);

// Each G2 point: 256 bytes
// - x.c0: 64 bytes (Fp, padded)
// - x.c1: 64 bytes (Fp)
// - y.c0: 64 bytes (Fp)
// - y.c1: 64 bytes (Fp)

await bls12_381.g2Add(input, output);
Input Format: 512 bytes (two G2 points) Output Format: 256 bytes (result G2 point)

G2 Scalar Multiplication

// Multiply G2 point by scalar
const input = new Uint8Array(288); // point (256) || scalar (32)
const output = new Uint8Array(256);

await bls12_381.g2Mul(input, output);
Input Format: 288 bytes
  • Bytes 0-255: G2 point (x.c0 || x.c1 || y.c0 || y.c1)
  • Bytes 256-287: Scalar (32-byte big-endian)

G2 Multi-Scalar Multiplication

// MSM for G2 points
const numPoints = 5;
const input = new Uint8Array(288 * numPoints);
const output = new Uint8Array(256);

await bls12_381.g2Msm(input, output);

Pairing Operations

Optimal Ate Pairing

// Compute pairing(s) and check if product equals 1
const pairs = 2; // Number of (G1, G2) pairs
const input = new Uint8Array(384 * pairs);
const output = Bytes32();

// Each pair: 384 bytes
// - G1 point: 128 bytes (x || y, each 64 bytes padded)
// - G2 point: 256 bytes (x.c0 || x.c1 || y.c0 || y.c1)

await bls12_381.pairing(input, output);

// Output interpretation:
// - 0x00...01: Pairing product equals 1 (valid)
// - 0x00...00: Pairing product not equal to 1 (invalid)
Input Format: Multiple of 384 bytes
  • Each pair: G1 (128 bytes) || G2 (256 bytes)
Output Format: 32 bytes
  • Last byte 0x01: Pairing check passed
  • Last byte 0x00: Pairing check failed

Pairing Check (BLS Signature Verification)

// Verify BLS signature
async function verifyBLSSignature(
  signature: Uint8Array,  // G1 point (128 bytes)
  publicKey: Uint8Array,  // G2 point (256 bytes)
  message: Uint8Array,    // Hashed to G1 (128 bytes)
  generator: Uint8Array   // G2 generator (256 bytes)
): Promise<boolean> {
  // Check: e(signature, G2) = e(H(msg), pubkey)
  // Equivalent: e(signature, G2) * e(-H(msg), pubkey) = 1

  const negatedMessage = negateG1(message);

  const input = new Uint8Array(768); // 2 pairs * 384 bytes
  input.set(signature, 0);        // Pair 1: signature, G2 gen
  input.set(generator, 128);
  input.set(negatedMessage, 384);  // Pair 2: -H(msg), pubkey
  input.set(publicKey, 512);

  const output = Bytes32();
  await bls12_381.pairing(input, output);

  return output[31] === 0x01;
}

Point Mapping

Map Field Element to G1

import { bls12_381 } from '@tevm/voltaire/crypto';

// Hash to curve: map Fp element to G1 point
const fpElement = Bytes64(); // Padded field element
const g1Point = new Uint8Array(128);

await bls12_381.mapFpToG1(fpElement, g1Point);
Use case: Hash-to-curve for deterministic point generation

Map Field Element to G2

// Map Fp2 element to G2 point
const fp2Element = new Uint8Array(128); // c0 (64) || c1 (64)
const g2Point = new Uint8Array(256);

await bls12_381.mapFp2ToG2(fp2Element, g2Point);

Use Cases

BLS Signature Aggregation

// Aggregate multiple signatures
async function aggregateSignatures(signatures: Uint8Array[]): Promise<Uint8Array> {
  let aggregated = signatures[0];

  for (let i = 1; i < signatures.length; i++) {
    const input = new Uint8Array(256);
    input.set(aggregated, 0);
    input.set(signatures[i], 128);

    const output = new Uint8Array(128);
    await bls12_381.g1Add(input, output);
    aggregated = output;
  }

  return aggregated;
}

// Batch verify aggregated signature
async function batchVerifyAggregated(
  aggregatedSignature: Uint8Array,
  publicKeys: Uint8Array[],
  messages: Uint8Array[]
): Promise<boolean> {
  // Aggregate public keys
  const aggregatedPubKey = await aggregateG2Points(publicKeys);

  // Aggregate messages (hash to curve)
  const aggregatedMessage = await aggregateG1Points(messages);

  // Single pairing check
  return verifyBLSSignature(
    aggregatedSignature,
    aggregatedPubKey,
    aggregatedMessage,
    G2_GENERATOR
  );
}

Ethereum 2.0 Validator Signatures

// Verify sync committee aggregate signature
async function verifySyncCommitteeSignature(
  signature: Uint8Array,        // Aggregated BLS signature
  publicKeys: Uint8Array[],     // Validator public keys
  signingRoot: Uint8Array       // Block root being signed
): Promise<boolean> {
  // Map signing root to G1
  const message = await hashToG1(signingRoot);

  // Aggregate validator public keys
  const aggregatedPubKey = await aggregateG2Points(publicKeys);

  // Verify aggregated signature
  return verifyBLSSignature(signature, aggregatedPubKey, message, G2_GENERATOR);
}

Implementation Details

C Library (BLST - Production)

  • Library: BLST (Supranational)
  • Location: lib/blst/ (git submodule)
  • Status: Audited, production-grade
  • Performance: Assembly-optimized for x86_64, ARM64
  • Features:
    • Constant-time operations
    • Side-channel resistant
    • Multi-scalar multiplication (Pippenger)
    • Compressed point support
Why BLST?
  • Official Ethereum Foundation recommendation
  • Used in all major Ethereum clients (Prysm, Lighthouse, Teku)
  • Extensive security audits (Trail of Bits, NCC Group)
  • Performance leader in benchmarks

Zig FFI Wrapper

  • Location: src/crypto/crypto.zig
  • Purpose: Safe Zig bindings to BLST C library
  • Features:
    • Error handling wrapper
    • Memory safety
    • Type-safe point validation
// Zig wrapper for BLS12-381 operations
pub const bls12_381 = struct {
    pub fn g1Add(input: []const u8, output: []u8) Error!void { ... }
    pub fn g1Mul(input: []const u8, output: []u8) Error!void { ... }
    pub fn pairing(input: []const u8, output: []u8) Error!void { ... }
    // ...
};

TypeScript API

  • Location: src/crypto/crypto.zig (exported via FFI)
  • Runtime: Node.js native, Bun FFI, WASM
  • Validation: Automatic point validation on all operations

WASM Limitations

BLST unavailable in WASM - C library requires native compilation. Alternatives:
  1. noble/curves: Pure TS implementation (slower, ~10x)
  2. Stub implementations: Return errors for unsupported platforms
// WASM builds may not support BLS12-381
import { bls12_381 } from '@tevm/voltaire/crypto';

try {
  await bls12_381.g1Add(input, output);
} catch (error) {
  console.error("BLS12-381 not available in WASM");
}

Security Considerations

Production Requirements:
  • Use BLST library (audited, constant-time)
  • Validate all deserialized points
  • Check subgroup membership (especially G2)
  • Verify scalar range [1, r-1]
Point Validation:
// BLST performs automatic validation:
// - Point on curve check
// - Subgroup membership check (G2)
// - Infinity point handling

// Invalid points will return error
try {
  await bls12_381.g1Add(input, output);
} catch (error) {
  // Invalid point or computation failure
}
Signature Security:
  • Rogue key attacks: Prevented by proof-of-possession
  • Signature malleability: Use canonical point representations
  • Domain separation: Hash with context string for different message types
Timing Side-Channels:
  • BLST uses constant-time operations
  • No branching on secret data
  • Resistant to cache-timing attacks

Performance

Native (BLST on x86_64):
  • G1 addition: ~0.015ms
  • G1 multiplication: ~0.08ms
  • G2 addition: ~0.025ms
  • G2 multiplication: ~0.2ms
  • Pairing: ~1.2ms
  • Pairing check (2 pairs): ~2ms
  • G1 MSM (100 points): ~8ms
Optimization Tips:
  • Batch operations with MSM
  • Precompute static points
  • Use compressed point formats
  • Aggregate signatures before verification

Constants

// Curve order (scalar field modulus)
const FR_MOD = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001n;

// Base field modulus (381 bits)
const FP_MOD = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaabn;

// Embedding degree
const EMBEDDING_DEGREE = 12;

// Security level
const SECURITY_BITS = 128;

// G1 generator (compressed)
const G1_GENERATOR_COMPRESSED = new Uint8Array([
  0x97, 0xf1, 0xd3, 0xa7, /* ... 48 bytes total */
]);

// G2 generator (compressed)
const G2_GENERATOR_COMPRESSED = new Uint8Array([
  0x93, 0xe0, 0x2b, 0x6c, /* ... 96 bytes total */
]);

EIP-2537 Precompiles

Status: Proposed (not yet activated on mainnet) Precompile Addresses:
  • 0x0b: BLS12_G1ADD
  • 0x0c: BLS12_G1MUL
  • 0x0d: BLS12_G1MULTIEXP
  • 0x0e: BLS12_G2ADD
  • 0x0f: BLS12_G2MUL
  • 0x10: BLS12_G2MULTIEXP
  • 0x11: BLS12_PAIRING
  • 0x12: BLS12_MAP_FP_TO_G1
  • 0x13: BLS12_MAP_FP2_TO_G2
Gas Costs (EIP-2537):
  • G1 addition: 500 gas
  • G1 multiplication: 12,000 gas
  • Pairing (base): 115,000 gas
  • Pairing (per pair): 23,000 gas

References