This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.
Overview
Address: 0x0000000000000000000000000000000000000013
Introduced: Prague (EIP-2537)
EIP: EIP-2537
The BLS12-381 Map Fp2 to G2 precompile maps an element from the quadratic extension field Fp2 to a point on the G2 curve. This is the G2 equivalent of the G1 hash-to-curve operation, essential for BLS signatures where messages are hashed to G2 (the standard BLS variant).
G2 operates over Fp2, the quadratic extension field Fp2 = Fp[u]/(u²+1), providing additional algebraic structure required for pairing-based cryptography. Most BLS signature schemes hash messages to G2 rather than G1 for efficiency reasons.
Hash-to-Curve for G2
The complete hash-to-curve process for G2:
- Hash message to two Fp2 elements using hash_to_field (external)
- Map each Fp2 element to G2 point using this precompile (0x13)
- Add the two points (use G2_ADD precompile 0x0e)
- Clear cofactor to ensure point is in correct subgroup (if needed)
This precompile implements step 2: mapping a single Fp2 element to a G2 curve point.
Extension Field Fp2
Fp2 is constructed as Fp[u]/(u²+1), where:
- Elements have form:
a = c0 + c1*u
- Addition:
(a0 + a1*u) + (b0 + b1*u) = (a0+b0) + (a1+b1)*u
- Multiplication:
(a0 + a1*u) * (b0 + b1*u) = (a0*b0 - a1*b1) + (a0*b1 + a1*b0)*u
- Constraint:
u² = -1
Each component c0, c1 is an element of the base field Fp.
Gas Cost
Fixed cost: 75,000 gas (current implementation)
Note: The EIP-2537 specification proposes 23,800 gas for this operation. The current implementation uses 75,000 gas which may represent a pre-repricing value or conservative estimate. Consult the latest EIP-2537 status for the finalized gas cost. Code uses 75,000 in /Users/williamcory/voltaire/src/precompiles/precompiles.ts line 1196.
Higher than G1 mapping (5,500 gas) due to:
- Larger field (Fp2 vs Fp)
- More complex curve arithmetic
- G2 point operations are inherently more expensive
Still much cheaper than scalar multiplication operations.
Offset | Length | Description
-------|--------|-------------
0 | 64 | c0: First component of Fp2 element (big-endian)
64 | 64 | c1: Second component of Fp2 element (big-endian)
Total input length: 128 bytes (exactly)
Fp2 element encoding:
- Element is
c0 + c1*u where u² = -1
- Each component must be < field modulus p
- Both components big-endian, left-padded to 64 bytes
- All values where c0, c1 ∈ [0, p-1] are valid
BLS12-381 field modulus p:
0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
Offset | Length | Description
-------|--------|-------------
0 | 64 | x.c0: First component of x coordinate
64 | 64 | x.c1: Second component of x coordinate
128 | 64 | y.c0: First component of y coordinate
192 | 64 | y.c1: Second component of y coordinate
Total output length: 256 bytes (G2 point in uncompressed form)
G2 point structure:
- x = x.c0 + x.c1*u (Fp2 element)
- y = y.c0 + y.c1*u (Fp2 element)
- Satisfies curve equation: y² = x³ + 4(1+u)
Usage Examples
TypeScript
import { execute, PrecompileAddress } from '@tevm/voltaire/precompiles';
import { Hardfork } from '@tevm/voltaire/primitives/Hardfork';
import { Keccak256 } from '@tevm/voltaire/Keccak256';
// Hash message to G2 point (standard BLS signature scheme)
function hashToG2Point(message: Uint8Array): Uint8Array {
// Step 1: Hash message to two field elements (simplified)
const hash1 = Keccak256.hash(message);
const hash2 = Keccak256.hash(hash1);
// Create two Fp2 elements (128 bytes each: c0 + c1)
const u0 = new Uint8Array(128);
const u0c0 = Bytes64(hash1); // c0 component from hash
u0.set(u0c0, 0);
// c1 component stays zero for simplification (bytes 64-127)
const u1 = new Uint8Array(128);
const u1c0 = Bytes64(hash2); // c0 component from hash
u1.set(u1c0, 0);
// Step 2: Map both to G2
const q0Result = execute(
PrecompileAddress.BLS12_MAP_FP2_TO_G2,
u0,
75000n, // Current implementation gas cost
Hardfork.PRAGUE
);
const q1Result = execute(
PrecompileAddress.BLS12_MAP_FP2_TO_G2,
u1,
75000n, // Current implementation gas cost
Hardfork.PRAGUE
);
if (!q0Result.success || !q1Result.success) {
throw new Error('Mapping failed');
}
// Step 3: Add points (use G2_ADD precompile 0x0e)
const addInput = new Uint8Array(512);
addInput.set(q0Result.output, 0);
addInput.set(q1Result.output, 256);
const addResult = execute(
PrecompileAddress.BLS12_G2_ADD,
addInput,
800n,
Hardfork.PRAGUE
);
return addResult.output; // 256-byte G2 point
}
// BLS signature: Sign by multiplying message point by secret key
const message = new TextEncoder().encode("Sign this");
const messagePoint = hashToG2Point(message);
console.log('Message hashed to G2:', messagePoint.length, 'bytes');
Zig
const std = @import("std");
const precompiles = @import("precompiles");
const crypto = @import("crypto");
/// Hash message to G2 point for BLS signatures
pub fn hashToG2(
allocator: std.mem.Allocator,
message: []const u8,
) ![]u8 {
// Step 1: Hash to two Fp2 elements
var u0: [128]u8 = undefined;
var u1: [128]u8 = undefined;
@memset(&u0, 0);
@memset(&u1, 0);
// Hash message
const hash1 = crypto.Crypto.keccak256(message);
const hash2 = crypto.Crypto.keccak256(&hash1);
// Create Fp2 elements (simplified - c1 components zero)
@memcpy(u0[96..128], hash1[0..32]);
@memcpy(u1[96..128], hash2[0..32]);
// Step 2: Map both to G2
const q0_result = try precompiles.bls12_map_fp2_to_g2.execute(
allocator,
&u0,
75000, // Current implementation gas cost
);
defer allocator.free(q0_result.output);
const q1_result = try precompiles.bls12_map_fp2_to_g2.execute(
allocator,
&u1,
75000, // Current implementation gas cost
);
defer allocator.free(q1_result.output);
// Step 3: Add points
var add_input = try allocator.alloc(u8, 512);
defer allocator.free(add_input);
@memcpy(add_input[0..256], q0_result.output);
@memcpy(add_input[256..512], q1_result.output);
const add_result = try precompiles.bls12_g2_add.execute(
allocator,
add_input,
800,
);
return add_result.output; // Caller owns memory
}
test "hash to G2" {
const msg = "Hello BLS!";
const point = try hashToG2(std.testing.allocator, msg);
defer std.testing.allocator.free(point);
try std.testing.expectEqual(@as(usize, 256), point.len);
}
Error Conditions
- Out of gas: Gas limit less than 75,000 (current implementation)
- Invalid input length: Input not exactly 128 bytes
- Field element overflow: c0 >= p or c1 >= p
- Invalid Fp2 encoding: Malformed extension field element
All Fp2 elements with both components in range [0, p-1] are valid inputs.
Use Cases
BLS Signature Scheme (Standard Variant)
Standard BLS hashes messages to G2, signs in G2:
// Secret key: sk ∈ Zr (scalar)
// Public key: PK = sk * G1 (point in G1)
// Signature: sig = sk * H(m) where H(m) ∈ G2
// Verify: e(PK, H(m)) = e(G1, sig)
function blsSign(secretKey: bigint, message: Uint8Array): Uint8Array {
// Hash message to G2
const h = hashToG2Point(message);
// Multiply by secret key (use G2_MUL precompile 0x0f)
const mulInput = new Uint8Array(288);
mulInput.set(h, 0);
// Set scalar (32 bytes at offset 256)
// ... secretKey encoding
const result = execute(
PrecompileAddress.BLS12_G2_MUL,
mulInput,
45000n,
Hardfork.PRAGUE
);
return result.output; // Signature in G2
}
Aggregate Signatures
Multiple signatures on different messages:
// Each signer signs their message
const sig1 = blsSign(sk1, msg1); // H(msg1)^sk1
const sig2 = blsSign(sk2, msg2); // H(msg2)^sk2
// Aggregate signatures (point addition in G2)
const aggInput = new Uint8Array(512);
aggInput.set(sig1, 0);
aggInput.set(sig2, 256);
const aggSig = execute(
PrecompileAddress.BLS12_G2_ADD,
aggInput,
800n,
Hardfork.PRAGUE
).output;
// Verify aggregate:
// e(PK1, H(msg1)) * e(PK2, H(msg2)) = e(G1, aggSig)
Threshold Signatures
Distribute signing authority across multiple parties:
// Each party holds share of secret key
// Hash message to G2 once
const messagePoint = hashToG2Point(message);
// Each party signs with their share
const shares = parties.map(party =>
party.signWithShare(messagePoint)
);
// Combine t-of-n shares to reconstruct signature
const signature = lagrangeInterpolate(shares);
Boneh-Lynn-Shacham Signatures
Original BLS paper construction:
- Short signatures (G2 points)
- Aggregation without interaction
- Batch verification
// Verify batch of signatures
function batchVerify(
publicKeys: Uint8Array[], // G1 points
messages: Uint8Array[],
signatures: Uint8Array[] // G2 points
): boolean {
// Compute pairings for each (PK, H(msg), sig)
// Product of all pairings should equal 1
// Use BLS12_PAIRING precompile (0x11)
}
Implementation Details
- Algorithm: Simplified SWU map for G2 curve
- Curve: BLS12-381 G2 over Fp2 (twist curve)
- Equation: y² = x³ + 4(1+u) where u² = -1
- Properties: Deterministic, constant-time, uniform distribution
- Zig: Uses blst library via C FFI
- TypeScript: Uses @noble/curves BLS12-381
G2 Curve Properties
G2 is the twist of the base curve:
- Defined over Fp2 instead of Fp
- Same group order as G1
- Larger representation (256 bytes vs 128 bytes)
- Slower arithmetic but richer structure for pairings
Why Hash to G2?
BLS signatures typically hash to G2 because:
- Verification efficiency: Public keys in G1 (smaller)
- Signature aggregation: Addition in G2 during signing
- Pairing efficiency: G1 in first position of pairing is faster
Alternative (hash to G1) is used when aggregating public keys instead.
Hash-to-Curve Standards
Implements mapping from:
Complete hash-to-curve (standards-compliant):
- hash_to_field: message → (u0, u1) where ui ∈ Fp2
- map_to_curve: ui → Qi for i = 0,1 (this precompile)
- Q = Q0 + Q1 (use G2_ADD)
- P = clear_cofactor(Q)
Security Considerations
Constant-Time Execution
Critical for signature schemes:
- No timing leakage of field element values
- Uniform execution across all valid inputs
- Protects against side-channel attacks on secret keys
Two-map construction (u0, u1) ensures:
- Output distribution indistinguishable from random
- No bias toward specific curve points
- Security proofs require uniformity
Domain Separation Tags
Use unique DST per protocol:
const DST = "MY_PROTOCOL_V1_G2_HASH";
// Include in hash_to_field before calling precompile
Prevents cross-protocol attacks.
Subgroup Checking
After mapping, ensure point is in correct subgroup:
- G2 has cofactor h = 0x5d543a95414e7f1091d50792876a202cd91de4547085abaa68a205b2e5a7ddfa628f1cb4d9e82ef21537e293a6691ae1616ec6e786f0c70cf1c38e31c7238e5
- Clear cofactor by multiplying:
P = h * Q
- Or use cofactor clearing map (protocol-specific)
Gas Comparison
| Operation | Gas | Notes |
|---|
| Map Fp to G1 | 5,500 | Base field |
| Map Fp2 to G2 | 75,000 | Extension field (13.6x) |
| G1 Add | 500 | Point addition |
| G2 Add | 800 | Point addition |
| G1 Mul | 12,000 | Scalar multiplication |
| G2 Mul | 45,000 | Scalar multiplication |
G2 operations consistently ~3-13x more expensive than G1.
Complete Hash-to-G2 Cost
2 × MAP_FP2_TO_G2: 2 × 75,000 = 150,000
1 × G2_ADD: 1 × 800 = 800
Total: = 150,800 gas
Plus external hash_to_field computation. Note: If EIP-2537 repricing occurs (23,800 per map), total would be ~48,400 gas.
Test Vectors
Zero Fp2 Element
const input = new Uint8Array(128); // All zeros (0 + 0*u)
const result = execute(
PrecompileAddress.BLS12_MAP_FP2_TO_G2,
input,
75000n,
Hardfork.PRAGUE
);
// Should succeed with valid G2 point
console.log('Zero mapped to G2:', result.success);
Non-zero c0, Zero c1
const input = new Uint8Array(128);
input[63] = 1; // c0 = 1, c1 = 0
// Represents Fp2 element: 1 + 0*u
const result = execute(
PrecompileAddress.BLS12_MAP_FP2_TO_G2,
input,
75000n,
Hardfork.PRAGUE
);
console.log('Success:', result.success);
console.log('Point length:', result.output.length); // 256
Both Components Non-zero
const input = new Uint8Array(128);
input[63] = 2; // c0 = 2
input[127] = 3; // c1 = 3
// Represents: 2 + 3*u
const result = execute(
PrecompileAddress.BLS12_MAP_FP2_TO_G2,
input,
75000n,
Hardfork.PRAGUE
);
// Should produce different point than previous examples
Determinism Verification
const input = new Uint8Array(128);
input[63] = 42;
input[127] = 137;
const result1 = execute(PrecompileAddress.BLS12_MAP_FP2_TO_G2, input, 75000n, Hardfork.PRAGUE);
const result2 = execute(PrecompileAddress.BLS12_MAP_FP2_TO_G2, input, 75000n, Hardfork.PRAGUE);
// Same input must produce identical output
console.assert(result1.output.every((b, i) => b === result2.output[i]));