Guides/EVM/Precompiles

BLS12-381 verification

EIP-2537 curve operations, encodings, gas costs, and a Solidity BLS signature verification example.

View as Markdown

The runtime implements the seven precompiles from the final EIP-2537 specification. They become available on a network when the runtime containing this change is activated. A Frontier Git commit or node binary alone does not activate them.

AddressOperationInput bytesOutput bytesGas
0x0bG1 addition256128375
0x0cG1 multi-scalar multiplication (MSM)160 × k12812,000 × k × G1 discount / 1,000
0x0dG2 addition512256600
0x0eG2 MSM288 × k25622,500 × k × G2 discount / 1,000
0x0fPairing check384 × k3237,700 + 32,600 × k
0x10Map Fp to G1641285,500
0x11Map Fp2 to G212825623,800

Here k must be positive. The MSM discounts are the separate G1 and G2 tables in EIP-2537; the entries for 128 points also apply to larger inputs. Division rounds down. An MSM with one point performs ordinary scalar multiplication. The gas above excludes the EVM call and transaction overhead.

These are stateless, selectorless precompiles. Send packed bytes directly using staticcall, without a Solidity function selector or an ABI bytes length/offset wrapper. CALL, STATICCALL, DELEGATECALL, and CALLCODE are supported. They have no Bittensor administrative enable/disable switch.

Fp coordinates are 64-byte big-endian integers, strictly less than the field modulus; the first 16 bytes must be zero. Fp2 is encoded as c0 || c1. A G1 point is x || y (128 bytes), and a G2 point is x.c0 || x.c1 || y.c0 || y.c1 (256 bytes). All-zero coordinates encode infinity. Scalars are 32-byte big-endian integers and need not be less than the subgroup order. These encodings differ from compressed BLS public keys and signatures.

MSM and pairing reject points outside the prime-order subgroup. Addition only checks curve membership, as required by EIP-2537. Invalid lengths, invalid coordinates, and failed subgroup checks cause exceptional failure and burn all gas forwarded to that call. A valid pairing equation that does not hold returns 32-byte zero; an equation that holds returns 32-byte one.

Always check both the success flag and the expected output length: calling an address before its precompile is activated can succeed with empty return data.

(bool ok, bytes memory output) = address(0x0f).staticcall(pairingInput);
require(ok && output.length == 32, "pairing unavailable or invalid input");
bool verified = abi.decode(output, (uint256)) == 1;

Signature verification example

precompiles/tests/fixtures/Bls12381Example.sol is a complete single-signature example using public keys in G1, signatures in G2, and the BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_ ciphersuite. It expands the message with SHA-256, reduces four 512-bit values to field elements using Modexp, maps two Fp2 elements with 0x11, adds them with 0x0d, and checks the two-pair equation with 0x0f. It rejects infinity keys and signatures.

The example expects uncompressed EIP-2537 point encoding. It does not implement aggregate verification, compressed-point decoding, or proof-of-possession key registration. Use a ciphersuite and domain separation appropriate to your protocol; an arbitrary caller-supplied curve point is not a substitute for hashing the intended message.

Rust tests execute the compiled Solidity example through Frontier, using independent Noble BLS signatures for empty, short, and multi-block messages. They also cover wrong messages, infinity, invalid lengths, all four call modes, and exceptional gas consumption. The fixture generator pins solc 0.8.21 and Noble 1.9.7 and records the regeneration command in its source.

The precompile tests additionally run the pinned Geth conformance vectors for all seven operations, including exact output and gas checks. Existing Solidity interfaces, selectors, and Bittensor precompile addresses remain unchanged.