Skip to main content
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: 0x0000000000000000000000000000000000000012 Introduced: Prague (EIP-2537) EIP: EIP-2537 The BLS12-381 Map Fp to G1 precompile maps a field element from the base field Fp to a point on the G1 curve. This is a core building block for hash-to-curve operations, enabling deterministic point generation for BLS signatures, VRFs, and other cryptographic protocols. Hash-to-curve provides a way to hash arbitrary messages to curve points in a way that is:
  • Deterministic: Same input always produces same output
  • Uniform: Output distribution is indistinguishable from random
  • One-way: Cannot reverse the mapping
  • Collision-resistant: Hard to find different inputs mapping to same point

Hash-to-Curve Overview

The complete hash-to-curve process typically involves:
  1. Hash message to field elements using hash_to_field (external)
  2. Map field elements to curve points using this precompile (0x12)
  3. Clear cofactor to ensure point is in correct subgroup (if needed)
This precompile implements step 2: the deterministic mapping from a field element to a G1 curve point.

Gas Cost

Fixed cost: 5,500 gas (constant, independent of input) Much cheaper than elliptic curve operations since it’s a single mapping operation without scalar multiplication.

Input Format

Offset | Length | Description
-------|--------|-------------
0      | 64     | Field element in Fp (big-endian, padded)
Total input length: 64 bytes (exactly) Field element constraints:
  • Must be < field modulus p
  • Big-endian encoding
  • Left-padded with zeros to 64 bytes
  • All values 0 to p-1 are valid inputs
BLS12-381 base field modulus p:
0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
(381-bit prime, ~48 bytes, padded to 64)

Output Format

Offset | Length | Description
-------|--------|-------------
0      | 64     | x coordinate (big-endian, padded)
64     | 64     | y coordinate (big-endian, padded)
Total output length: 128 bytes (G1 point in uncompressed form) Output is always a valid G1 point on the curve. The mapping ensures:
  • Point is on curve: y² = x³ + 4
  • Point is in correct subgroup (after cofactor clearing if protocol requires)
  • Mapping is deterministic and injective

Usage Examples

TypeScript

import { execute, PrecompileAddress } from '@tevm/voltaire/precompiles';
import { Hardfork } from '@tevm/voltaire/primitives/Hardfork';
import { Keccak256 } from '@tevm/voltaire/Keccak256';

// Map a hash to a G1 point for signature schemes
function hashToG1Point(message: Uint8Array): Uint8Array {
  // Step 1: Hash message to field element
  const hash = Keccak256.hash(message);

  // Pad to 64 bytes (field element size)
  const fpElement = Bytes64(hash); // Automatically handles padding

  // Step 2: Map to G1 using precompile
  const result = execute(
    PrecompileAddress.BLS12_MAP_FP_TO_G1,
    fpElement,
    5500n,
    Hardfork.PRAGUE
  );

  if (!result.success) {
    throw new Error(`Map failed: ${result.error}`);
  }

  return result.output; // 128-byte G1 point
}

// Use in BLS signature scheme
const message = new TextEncoder().encode("Sign this message");
const messagePoint = hashToG1Point(message);
console.log('Message mapped to G1 point:', messagePoint);

Zig

const std = @import("std");
const precompiles = @import("precompiles");
const crypto = @import("crypto");

/// Hash message to G1 point for BLS signatures
pub fn hashToG1(
    allocator: std.mem.Allocator,
    message: []const u8,
) ![]u8 {
    // Step 1: Hash to field element
    var fp_element: [64]u8 = undefined;
    @memset(&fp_element, 0);

    const hash = crypto.Crypto.keccak256(message);
    // Right-align hash in 64-byte buffer
    @memcpy(fp_element[32..64], &hash);

    // Step 2: Map to G1
    const result = try precompiles.bls12_map_fp_to_g1.execute(
        allocator,
        &fp_element,
        5500,
    );

    return result.output; // Caller owns memory
}

test "hash message to G1" {
    const msg = "Hello BLS12-381";
    const point = try hashToG1(std.testing.allocator, msg);
    defer std.testing.allocator.free(point);

    try std.testing.expectEqual(@as(usize, 128), point.len);
    // Point should not be point at infinity
    const is_zero = for (point) |byte| {
        if (byte != 0) break false;
    } else true;
    try std.testing.expect(!is_zero);
}

Error Conditions

  • Out of gas: Gas limit less than 5,500
  • Invalid input length: Input not exactly 64 bytes
  • Field element overflow: Input value >= field modulus p
  • Invalid encoding: Malformed field element
Note: All field elements in range [0, p-1] are valid inputs and will successfully map to G1 points.

Use Cases

BLS Signature Hash-to-Curve

BLS signatures require hashing messages to curve points:
// BLS signature: sig = H(m)^sk where H maps to G2
// For G1 variant: sig = sk * H(m) where H maps to G1

function signMessage(secretKey: bigint, message: Uint8Array): Uint8Array {
  // Hash message to G1 point
  const messagePoint = hashToG1(message);

  // Multiply by secret key (use G1_MUL precompile 0x0c)
  // signature = secretKey * messagePoint
  // ...
  return signature;
}

Verifiable Random Functions (VRF)

VRFs use hash-to-curve for deterministic randomness:
// VRF: Prove you know secret key that produces output
// Gamma = H(alpha)^sk
// Proof = NIZK that discrete logs match

function vrfProve(secretKey: bigint, alpha: Uint8Array) {
  const h = hashToG1(alpha); // H(alpha)
  // Gamma = sk * h (use G1_MUL precompile)
  // Generate NIZK proof...
}

Identity-Based Encryption

Map identities (email addresses, etc.) to public keys:
function identityToPublicKey(identity: string): Uint8Array {
  const identityBytes = new TextEncoder().encode(identity);
  return hashToG1(identityBytes);
}

// Now can encrypt to "[email protected]" without prior key exchange
const alicePubKey = identityToPublicKey("[email protected]");

Threshold Cryptography

Deterministic point generation for distributed key generation:
function generateCommitment(coefficientIndex: number): Uint8Array {
  const input = Bytes64();
  new DataView(input.buffer).setBigUint64(56, BigInt(coefficientIndex), false);

  const result = execute(
    PrecompileAddress.BLS12_MAP_FP_TO_G1,
    input,
    5500n,
    Hardfork.PRAGUE
  );

  return result.output;
}

Implementation Details

  • Algorithm: Simplified SWU (Shallue-van de Woestijne-Ulas) map
  • Curve: BLS12-381 G1 over Fp (equation: y² = x³ + 4)
  • Properties: Deterministic, injective (one-to-one), constant-time
  • Zig: Uses blst library implementation via C FFI
  • TypeScript: Uses @noble/curves BLS12-381 hash-to-curve

Simplified SWU Method

The map uses an isogeny-based approach:
  1. Map Fp element to point on isogenous curve E’
  2. Evaluate isogeny map to get point on target curve E
  3. Result is valid G1 point
This provides better distribution properties than older try-and-increment methods.

Hash-to-Curve Standards

This precompile implements the mapping function from: For complete hash-to-curve:
  1. Use hash_to_field to get two field elements u₀, u₁
  2. Map both to curve: Q₀ = map(u₀), Q₁ = map(u₁)
  3. Add points: Q = Q₀ + Q₁
  4. Clear cofactor: P = clear_cofactor(Q)
This precompile handles step 2. Steps 1, 3, 4 done in application code.

Security Considerations

Constant-Time Execution

The mapping must be constant-time to prevent timing side-channels:
  • No branches based on input value
  • Uniform execution path for all inputs
  • Protects secret keys in signature schemes

Distribution Uniformity

The map produces points with distribution indistinguishable from random:
  • Important for VRF security
  • Prevents bias in cryptographic protocols
  • Two-map approach (u₀, u₁) improves uniformity

Domain Separation

Different protocols should use different domain separation tags:
const DST = "MY_PROTOCOL_V1_HASH_TO_G1";
// Include DST in hash_to_field step before calling this precompile

Comparison with Other Approaches

Try-and-Increment (Legacy)

  • Hash + point validation loop
  • Variable time (security risk)
  • Non-uniform distribution
  • Not recommended

Simplified SWU (This Precompile)

  • Constant time
  • Uniform distribution
  • Standards-compliant
  • Recommended

Test Vectors

Zero Element

const input = Bytes64(); // All zeros
const result = execute(
  PrecompileAddress.BLS12_MAP_FP_TO_G1,
  input,
  5500n,
  Hardfork.PRAGUE
);
// Should succeed with valid G1 point
console.log('Mapped point:', result.output);

Maximum Field Element

// p-1 is maximum valid input
const input = Bytes64();
input.set([
  0x1a, 0x01, 0x11, 0xea, 0x39, 0x7f, 0xe6, 0x9a,
  0x4b, 0x1b, 0xa7, 0xb6, 0x43, 0x4b, 0xac, 0xd7,
  0x64, 0x77, 0x4b, 0x84, 0xf3, 0x85, 0x12, 0xbf,
  0x67, 0x30, 0xd2, 0xa0, 0xf6, 0xb0, 0xf6, 0x24,
  0x1e, 0xab, 0xff, 0xfe, 0xb1, 0x53, 0xff, 0xff,
  0xb9, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xaa, 0xaa,
], 16); // Right-align in 64 bytes

const result = execute(
  PrecompileAddress.BLS12_MAP_FP_TO_G1,
  input,
  5500n,
  Hardfork.PRAGUE
);
// Should succeed

Determinism Test

const input = Bytes64();
input[63] = 42;

const result1 = execute(PrecompileAddress.BLS12_MAP_FP_TO_G1, input, 5500n, Hardfork.PRAGUE);
const result2 = execute(PrecompileAddress.BLS12_MAP_FP_TO_G1, input, 5500n, Hardfork.PRAGUE);

// Same input always produces same output
console.assert(result1.output.every((b, i) => b === result2.output[i]));