// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @title BLS12381 — thin, spec-correct wrappers over AERE's LIVE EIP-2537 precompiles. * * @notice AERE Network (chain 2800, Prague) exposes the EIP-2537 BLS12-381 precompiles * natively. This library builds the exact EIP-2537 wire encodings and delegates * all field/curve math to those precompiles. It also implements RFC-9380 * hash-to-curve for G2 (expand_message_xmd over SHA-256, hash_to_field via the * modexp precompile, SSWU + isogeny + cofactor clear via MAP_FP2_TO_G2), so a * message can be mapped to a G2 point ON CHAIN with no trusted hint. * * Point encodings follow EIP-2537 exactly: * - Fp element = 64 bytes (16 zero bytes || 48-byte big-endian value) * - G1 point = 128 bytes (x || y), infinity = 128 zero bytes * - Fp2 element = 128 bytes (c0 || c1) * - G2 point = 256 bytes (x.c0 || x.c1 || y.c0 || y.c1), infinity = 256 zeros * * @dev SECURITY / HONESTY. BLS12-381 is a CLASSICAL, pairing-friendly curve and is * NOT post-quantum. This library is the INBOUND (Ethereum -> AERE) crypto: it * inherits Ethereum's sync-committee security model, which is classical and * quantum-vulnerable. It says nothing about AERE's own consensus (QBFT / ECDSA) * or AERE's PQC precompiles. See AereEthLightClient for the full model. */ library BLS12381 { // ---- EIP-2537 precompile addresses (Prague canonical) -------------------- address internal constant G1ADD = address(uint160(0x0b)); address internal constant G2ADD = address(uint160(0x0d)); address internal constant PAIRING = address(uint160(0x0f)); address internal constant MAP_FP2_TO_G2 = address(uint160(0x11)); // ---- Legacy precompiles used by hash-to-curve --------------------------- address internal constant SHA256 = address(uint160(0x02)); address internal constant MODEXP = address(uint160(0x05)); // Field modulus p of BLS12-381, big-endian 48 bytes. bytes internal constant P = hex"1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab"; uint256 internal constant G1_LEN = 128; uint256 internal constant G2_LEN = 256; // ------------------------------------------------------------------------- // curve operations // ------------------------------------------------------------------------- /// @notice G1 point addition via precompile 0x0b. `a`,`b` are 128-byte G1 encodings. function g1Add(bytes memory a, bytes memory b) internal view returns (bytes memory out) { require(a.length == G1_LEN && b.length == G1_LEN, "BLS:g1add len"); out = _call(G1ADD, bytes.concat(a, b), G1_LEN); } /// @notice G2 point addition via precompile 0x0d. `a`,`b` are 256-byte G2 encodings. function g2Add(bytes memory a, bytes memory b) internal view returns (bytes memory out) { require(a.length == G2_LEN && b.length == G2_LEN, "BLS:g2add len"); out = _call(G2ADD, bytes.concat(a, b), G2_LEN); } /// @notice Map an Fp2 element (two 64-byte Fp coords c0,c1) to G2 via precompile 0x11. /// The precompile applies SSWU + the 3-isogeny + cofactor clearing, so the /// result is a valid member of the G2 prime-order subgroup. function mapFp2ToG2(bytes memory fp2) internal view returns (bytes memory out) { require(fp2.length == 128, "BLS:map len"); out = _call(MAP_FP2_TO_G2, fp2, G2_LEN); } /// @notice EIP-2537 pairing check. `input` is k concatenated (G1 || G2) pairs /// (k * 384 bytes). Returns true iff the product of pairings equals one. function pairing(bytes memory input) internal view returns (bool) { require(input.length % 384 == 0 && input.length != 0, "BLS:pair len"); bytes memory ret = _call(PAIRING, input, 32); return ret[31] == 0x01; } // ------------------------------------------------------------------------- // signature verification (min-pubkey-size) // ------------------------------------------------------------------------- /// @notice Verify a BLS signature under the Ethereum "min-pubkey-size" scheme: /// public key in G1 (128B), signature in G2 (256B). Checks /// e(-G1_generator, signature) * e(pubkey, H(message)) == 1 /// where H = hashToG2(message, dst). All heavy math runs on the LIVE /// precompiles; the message is hashed to the curve on chain (no hint). /// @param pubkeyG1 aggregate (or single) public key, 128-byte G1 encoding /// @param message the 32-byte signing root (or any message) that was signed /// @param signatureG2 the signature, 256-byte G2 encoding /// @param dst the RFC-9380 domain separation tag used when signing function verify( bytes memory pubkeyG1, bytes memory message, bytes memory signatureG2, bytes memory dst ) internal view returns (bool) { require(pubkeyG1.length == G1_LEN, "BLS:pk len"); require(signatureG2.length == G2_LEN, "BLS:sig len"); bytes memory hm = hashToG2(message, dst); // pairing input = (-G1) || sig ++ pk || H(m) bytes memory input = bytes.concat(NEG_G1_GENERATOR(), signatureG2, pubkeyG1, hm); return pairing(input); } // ------------------------------------------------------------------------- // RFC-9380 hash-to-curve (G2) // ------------------------------------------------------------------------- /// @notice Hash an arbitrary message to a point in G2, per RFC-9380 /// (expand_message_xmd:SHA-256, SSWU_RO). Reproduces the reference /// behaviour of noble-curves bls12_381.G2.hashToCurve (validated). function hashToG2(bytes memory message, bytes memory dst) internal view returns (bytes memory) { // 256 bytes = count(2) * m(2) * L(64) bytes memory uniform = _expandMessageXmd(message, dst, 256); bytes memory fp2a = bytes.concat(_reduceModP(uniform, 0), _reduceModP(uniform, 64)); bytes memory fp2b = bytes.concat(_reduceModP(uniform, 128), _reduceModP(uniform, 192)); bytes memory q0 = mapFp2ToG2(fp2a); bytes memory q1 = mapFp2ToG2(fp2b); return g2Add(q0, q1); } /// @dev expand_message_xmd (RFC-9380 §5.4.1) with SHA-256, b_in_bytes=32. /// len_in_bytes must be a multiple of 32 and <= 255*32. function _expandMessageXmd(bytes memory message, bytes memory dst, uint256 lenInBytes) private view returns (bytes memory) { require(dst.length <= 255, "BLS:dst too long"); uint256 ell = lenInBytes / 32; // b_in_bytes = 32 require(ell * 32 == lenInBytes && ell <= 255, "BLS:xmd len"); bytes memory dstPrime = bytes.concat(dst, bytes1(uint8(dst.length))); bytes memory lBytes = bytes.concat(bytes1(uint8(lenInBytes >> 8)), bytes1(uint8(lenInBytes))); // b0 = H( Z_pad(64) || msg || l_i_b(2) || 0x00 || dst_prime ) bytes memory zPad = new bytes(64); bytes32 b0 = _sha256(bytes.concat(zPad, message, lBytes, bytes1(0x00), dstPrime)); // b1 = H( b0 || 0x01 || dst_prime ) bytes32 bPrev = _sha256(bytes.concat(b0, bytes1(0x01), dstPrime)); bytes memory out = new bytes(lenInBytes); _writeWord(out, 0, bPrev); for (uint256 i = 2; i <= ell; i++) { bytes32 xored = b0 ^ bPrev; bPrev = _sha256(bytes.concat(xored, bytes1(uint8(i)), dstPrime)); _writeWord(out, (i - 1) * 32, bPrev); } return out; } /// @dev Reduce the 64-byte big-endian value at `uniform[off:off+64]` modulo p, /// returning it as a 64-byte EIP-2537 Fp element (16 zero bytes || 48 bytes). /// Uses the modexp precompile: x mod p = x^1 mod p. function _reduceModP(bytes memory uniform, uint256 off) private view returns (bytes memory) { bytes memory base = new bytes(64); for (uint256 i = 0; i < 64; i++) base[i] = uniform[off + i]; // modexp input: base exp(0x01) mod(p) bytes memory input = bytes.concat( bytes32(uint256(64)), bytes32(uint256(1)), bytes32(uint256(48)), base, bytes1(0x01), P ); bytes memory r48 = _call(MODEXP, input, 48); // 48-byte reduced value bytes memory fp = new bytes(64); // 16 zero pad + 48 for (uint256 i = 0; i < 48; i++) fp[16 + i] = r48[i]; return fp; } // ------------------------------------------------------------------------- // helpers // ------------------------------------------------------------------------- function _sha256(bytes memory data) private view returns (bytes32 h) { bytes memory r = _call(SHA256, data, 32); assembly { h := mload(add(r, 32)) } } function _writeWord(bytes memory out, uint256 pos, bytes32 word) private pure { assembly { mstore(add(add(out, 32), pos), word) } } /// @dev staticcall a precompile, require success and an exact-length return. function _call(address to, bytes memory input, uint256 outLen) private view returns (bytes memory out) { bool ok; out = new bytes(outLen); assembly { ok := staticcall(gas(), to, add(input, 32), mload(input), add(out, 32), outLen) } require(ok, "BLS:precompile"); // guard against a short return (e.g. precompile missing on a non-Prague chain) require(out.length == outLen, "BLS:retlen"); } /// @dev The negation of the G1 generator, EIP-2537 encoded (128 bytes). /// x = generator.x ; y = p - generator.y. Precomputed constant. function NEG_G1_GENERATOR() internal pure returns (bytes memory) { return hex"0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f" hex"c3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb" hex"00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f2" hex"67816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca"; } }