aere-contracts/contracts/interop/lib/BLS12381.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

206 lines
10 KiB
Solidity

// 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: <baseLen=64><expLen=1><modLen=48> 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";
}
}