Skip to main content

BN254 (BN128)

Pairing-friendly elliptic curve implementation for zkSNARK verification and Ethereum’s Alt-BN128 precompiles (0x06-0x08).

Overview

BN254 (also known as BN128 or Alt-BN128) is a Barreto-Naehrig pairing-friendly elliptic curve widely used in zero-knowledge proof systems. It provides efficient pairing operations essential for zkSNARK verification, privacy-preserving protocols, and cryptographic applications requiring bilinear pairings. Ethereum Use Cases:
  • zkSNARKs: Zero-knowledge proof verification (Zcash, Tornado Cash, zkSync)
  • EIP-196: ECADD precompile (0x06) - G1 point addition
  • EIP-196: ECMUL precompile (0x07) - G1 scalar multiplication
  • EIP-197: ECPAIRING precompile (0x08) - Optimal ate pairing check
  • Privacy protocols: Confidential transactions, private voting systems

Quick Start

import * as BN254 from '@tevm/voltaire/crypto/bn254';

// G1 operations (base field)
const g1Gen = BN254.G1.generator();
const g1Doubled = BN254.G1.add(g1Gen, g1Gen);
const g1Scaled = BN254.G1.mul(g1Gen, 5n);

// G2 operations (extension field)
const g2Gen = BN254.G2.generator();
const g2Scaled = BN254.G2.mul(g2Gen, 3n);

// Pairing check (zkSNARK verification)
const isValid = BN254.Pairing.pairingCheck([
  [g1Scaled, g2Gen],
  [g1Gen, g2Scaled]
]);

Elliptic Curve Pairing Basics

Pairing-based cryptography uses a special bilinear map e: G1 × G2 → GT that enables:
  1. Bilinearity: e(aP, bQ) = e(P, Q)^(ab) - scalar multiplication distributes
  2. Non-degeneracy: e(G1, G2) ≠ 1 - generator pairing produces non-trivial result
  3. Computability: Pairing computable in polynomial time (optimal ate pairing)
Applications:
  • Identity-based encryption: Public keys derived from identities
  • Short signatures: BLS signatures with signature aggregation
  • zkSNARKs: Succinct non-interactive zero-knowledge proofs
  • Broadcast encryption: Efficient one-to-many encryption

API Reference

Field Elements

BN254 operates over two finite fields:

Base Field (Fp)

import * as Fp from '@tevm/voltaire/crypto/bn254/Fp';

// Field modulus (254 bits)
const p = Fp.MOD; // 21888242871839275222246405745257275088696311157297823662689037894645226208583n

// Field arithmetic
const a = 123n;
const b = 456n;
const sum = Fp.add(a, b);
const prod = Fp.mul(a, b);
const inv = Fp.inv(a);

Scalar Field (Fr)

import * as Fr from '@tevm/voltaire/crypto/bn254/Fr';

// Scalar field modulus (curve order)
const r = Fr.MOD; // 21888242871839275222246405745257275088548364400416034343698204186575808495617n

// Scalar arithmetic
const s1 = Fr.mod(1234567890n);
const s2 = Fr.mod(9876543210n);
const product = Fr.mul(s1, s2);

Extension Field (Fp2)

import * as Fp2 from '@tevm/voltaire/crypto/bn254/Fp2';

// Quadratic extension Fp2 = Fp[u]/(u^2 + 1)
const elem = Fp2.create(123n, 456n); // 123 + 456u
const squared = Fp2.square(elem);
const conjugate = Fp2.conjugate(elem); // 123 - 456u

Group Elements

G1 Points (Base Field)

import * as G1 from '@tevm/voltaire/crypto/bn254/G1';

// Generator point
const g = G1.generator(); // (1, 2)

// Point operations
const doubled = G1.double(g);
const sum = G1.add(g, doubled);
const scaled = G1.mul(g, 42n);
const negated = G1.negate(g);

// Point validation
const isOnCurve = G1.isOnCurve(g);
const isZero = G1.isZero(G1.infinity());

// Serialization (EIP-196 format)
const serialized = BN254.serializeG1(g); // 64 bytes: x || y
const deserialized = BN254.deserializeG1(serialized);
Curve equation: y^2 = x^3 + 3 over Fp

G2 Points (Extension Field)

import * as G2 from '@tevm/voltaire/crypto/bn254/G2';

// Generator point (Fp2 coordinates)
const g2 = G2.generator();

// Point operations
const doubled = G2.double(g2);
const sum = G2.add(g2, doubled);
const scaled = G2.mul(g2, 7n);
const negated = G2.negate(g2);

// Subgroup check
const inSubgroup = G2.isInSubgroup(g2);

// Serialization (EIP-197 format)
const serialized = BN254.serializeG2(g2); // 128 bytes: x_c0 || x_c1 || y_c0 || y_c1
const deserialized = BN254.deserializeG2(serialized);
Curve equation: y^2 = x^3 + 3/(9+u) over Fp2

Pairing Operations

Optimal Ate Pairing

import * as Pairing from '@tevm/voltaire/crypto/bn254/Pairing';

// Single pairing computation
const g1 = G1.generator();
const g2 = G2.generator();
const result = Pairing.pair(g1, g2); // Element in GT (Fp12)

// Check if pairing result equals 1
const isOne = Pairing.pairingResult.isOne(result);

Pairing Check (zkSNARK Verification)

// Verify pairing equation: e(P1, Q1) * e(P2, Q2) * ... = 1
const pairs = [
  [g1Point1, g2Point1],
  [g1Point2, g2Point2],
  [g1Point3, g2Point3]
];

const isValid = Pairing.pairingCheck(pairs);
Common pattern (Groth16 zkSNARK verification):
// Verify proof: e(-A, B) * e(alpha, beta) * e(C, delta) = 1
const isValidProof = Pairing.pairingCheck([
  [negatedA, proofB],
  [vkAlpha, vkBeta],
  [proofC, vkDelta]
]);

Serialization

G1 Point Format (64 bytes)

// EIP-196 format: x (32 bytes) || y (32 bytes)
const g1 = G1.generator();
const bytes = BN254.serializeG1(g1);
// bytes = [x_31, x_30, ..., x_0, y_31, y_30, ..., y_0]

// Point at infinity: (0, 0)
const infinity = G1.infinity();
const infinityBytes = BN254.serializeG1(infinity);
// infinityBytes = [0x00 * 64]

G2 Point Format (128 bytes)

// EIP-197 format: x_c0 (32) || x_c1 (32) || y_c0 (32) || y_c1 (32)
const g2 = G2.generator();
const bytes = BN254.serializeG2(g2);
// Fp2 element x = x_c0 + x_c1*u
// Fp2 element y = y_c0 + y_c1*u

Use Cases

zkSNARK Verification

// Verify Groth16 proof
function verifyGroth16Proof(
  proof: { A: G1Point, B: G2Point, C: G1Point },
  vk: { alpha: G1Point, beta: G2Point, gamma: G2Point, delta: G2Point },
  publicInputs: bigint[]
): boolean {
  // Compute linear combination of public inputs
  const vkX = computePublicInputLinearCombination(vk, publicInputs);

  // Pairing check: e(-A, B) * e(alpha, beta) * e(vkX, gamma) * e(C, delta) = 1
  return BN254.Pairing.pairingCheck([
    [BN254.G1.negate(proof.A), proof.B],
    [vk.alpha, vk.beta],
    [vkX, vk.gamma],
    [proof.C, vk.delta]
  ]);
}

EIP-196/197 Precompile Calls

// Direct precompile usage (via Zig/Rust)
import { bn254Add, bn254Mul, bn254Pairing } from '@tevm/voltaire/crypto/bn254';

// ECADD (0x06): Add two G1 points
const input1 = new Uint8Array(128); // p1_x || p1_y || p2_x || p2_y
const output1 = Bytes64();
bn254Add(input1, output1); // result_x || result_y

// ECMUL (0x07): Scalar multiply G1 point
const input2 = new Uint8Array(96); // p_x || p_y || scalar
const output2 = Bytes64();
bn254Mul(input2, output2);

// ECPAIRING (0x08): Pairing check
const input3 = new Uint8Array(192 * n); // n pairs of (g1_point || g2_point)
const isValid = bn254Pairing(input3); // boolean

Implementation Details

Rust Implementation (Production - Arkworks)

  • Library: arkworks (ark-bn254, ark-ec, ark-ff)
  • FFI: src/crypto/bn254_arkworks.zig
  • Status: Audited, production-ready
  • Performance: 3-5x faster than Zig implementation
  • Use: Recommended for production deployments
Why arkworks?
  • Battle-tested in Ethereum ecosystem
  • Constant-time operations (side-channel resistant)
  • Extensive security audits
  • Optimized assembly for critical paths

TypeScript Implementation (Reference)

  • Location: src/crypto/bn254/ (.js files)
  • Purpose: Pure TS reference, browser compatibility
  • Features:
    • Fp, Fp2 field arithmetic
    • G1, G2 point operations
    • Pairing computation
    • Serialization utilities

WASM Builds

Zig fallback: WASM builds use Zig implementation (arkworks unavailable in WASM). WASM performance is ~50% of native arkworks, but fully functional.

Security Considerations

Production Deployments:
  • Use arkworks (Rust) implementation for native builds
  • Audited, constant-time operations
  • Resistant to timing side-channels
Development/Testing:
  • Zig implementation suitable for testing
  • Pure implementation aids understanding
  • No known vulnerabilities, but unaudited
zkSNARK Security:
  • Verify trusted setup authenticity
  • Validate proof inputs (prevent malleability)
  • Check subgroup membership for G2 points
  • Ensure scalar values in valid range [1, r-1]
Point Validation:
// Always validate deserialized points
const g1 = BN254.deserializeG1(bytes);
if (!G1.isOnCurve(g1)) {
  throw new Error("Invalid G1 point");
}

const g2 = BN254.deserializeG2(bytes);
if (!G2.isOnCurve(g2) || !G2.isInSubgroup(g2)) {
  throw new Error("Invalid G2 point");
}

Performance

Native (Arkworks Rust):
  • ECADD: ~0.02ms
  • ECMUL: ~0.15ms
  • Pairing: ~1.5ms
  • Pairing check (2 pairs): ~2.5ms
WASM (Zig):
  • ECADD: ~0.05ms
  • ECMUL: ~0.3ms
  • Pairing: ~3ms
  • Pairing check (2 pairs): ~5ms

Constants

import { FP_MOD, FR_MOD, B_G1, G1_GENERATOR_X, G1_GENERATOR_Y } from '@tevm/voltaire/crypto/bn254';

// Field modulus (254 bits)
FP_MOD // 21888242871839275222246405745257275088696311157297823662689037894645226208583n

// Curve order (254 bits)
FR_MOD // 21888242871839275222246405745257275088548364400416034343698204186575808495617n

// G1 curve parameter: y^2 = x^3 + 3
B_G1 // 3n

// G1 generator point
G1_GENERATOR_X // 1n
G1_GENERATOR_Y // 2n

References