// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; /** * @title AereDrandQuicknetBLS * @notice Real on-chain BLS12-381 verification of drand beacon signatures for * the League of Entropy "quicknet" chain, built entirely on Aere's * live EIP-2537 precompiles. This library replaces the honesty gap in * the original AereRandomnessBeacon, whose `_verify` was a placeholder * that returned `true` for every input (it verified nothing). * * Target chain: drand quicknet (scheme id `bls-unchained-g1-rfc9380`). * chain hash : 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 * period : 3 seconds genesis: 1692803367 (unix) * public key : 96-byte compressed G2 point (see PUBKEY_G2 below, decompressed) * signatures : 48-byte compressed G1 points * message : SHA-256( round_number as 8-byte big-endian ) [UNCHAINED] * hash-to-curve DST: "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" * * The min-signature-size BLS check (signature in G1, public key in G2) is: * * e( H(m), pubkey ) == e( signature, G2_generator ) * * which we verify as a single EIP-2537 pairing product against 1: * * e( H(m), pubkey ) * e( signature, -G2_generator ) == 1 * * Every step is fail-closed. Any malformed input, any precompile that is * absent (returns empty) or returns the wrong-length output, any failed * pairing, all resolve to `false`. There is no path that returns `true` * without a genuine pairing equality. * * HONESTY / SCOPE NOTE: drand's BLS12-381 is CLASSICAL cryptography. It is * breakable by a sufficiently large quantum computer (Shor). This library * makes the beacon actually verify what it stores, which is a correctness and * honesty fix. It does NOT make the randomness post-quantum. Aere's PQC work * (Falcon/ML-DSA/SLH-DSA precompiles) is a separate track and is not involved * in drand verification. * * EIP-2537 precompiles used (all "Supported" on Aere mainnet, Prague parity): * 0x05 MODEXP (pre-Prague; modular exponentiation for hash-to-field reduction and sqrt) * 0x0b BLS12_G1ADD * 0x0f BLS12_PAIRING_CHECK * 0x10 BLS12_MAP_FP_TO_G1 */ library AereDrandQuicknetBLS { // ── precompile addresses ── address internal constant MODEXP = address(0x05); address internal constant BLS_G1ADD = address(0x0b); address internal constant BLS_PAIRING = address(0x0f); address internal constant BLS_MAP_FP_G1 = address(0x10); // ── field constants (BLS12-381 base field Fp), big-endian, 48 bytes ── // p = 0x1a0111ea...aaab bytes internal constant FIELD_MODULUS = hex"1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab"; // (p + 1) / 4 — sqrt exponent, valid because p ≡ 3 (mod 4). 48 bytes. bytes internal constant SQRT_EXP = hex"0680447a8e5ff9a692c6e9ed90d2eb35d91dd2e13ce144afd9cc34a83dac3d8907aaffffac54ffffee7fbfffffffeaab"; // RFC 9380 DST_prime = DST || I2OSP(len(DST), 1). len(DST) = 43 = 0x2b. // DST = "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" bytes internal constant DST_PRIME = hex"424c535f5349475f424c53313233383147315f584d443a5348412d3235365f535357555f524f5f4e554c5f2b"; // drand quicknet aggregated public key, decompressed to an EIP-2537 G2 // point (256 bytes: x.c0 || x.c1 || y.c0 || y.c1, each a 64-byte Fp element). // Decompressed off-chain from the canonical 96-byte compressed key // 83cf0f28...ece45a (verifiable at api.drand.sh//info). bytes internal constant PUBKEY_G2 = hex"000000000000000000000000000000000d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a0000000000000000000000000000000003cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d106451000000000000000000000000000000000e5db2b6bfbb01c867749cadffca88b36c24f3012ba09fc4d3022c5c37dce0f977d3adb5d183c7477c442b1f045152730000000000000000000000000000000001a714f2edb74119a2f2b0d5a7c75ba902d163700a61bc224ededd8e63aef7be1aaf8e93d7a9718b047ccddb3eb5d68b"; // Negated BLS12-381 G2 generator, EIP-2537 encoding (256 bytes). Constant // for all of BLS12-381; used so the pairing product checks against 1. bytes internal constant NEG_G2_GENERATOR = hex"00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb80000000000000000000000000000000013e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e000000000000000000000000000000000d1b3cc2c7027888be51d9ef691d77bcb679afda66c73f17f9ee3837a55024f78c71363275a75d75d86bab79f74782aa0000000000000000000000000000000013fa4d4a0ad8b1ce186ed5061789213d993923066dddaf1040bc3ff59f825c78df74f2d75467e25e0f55f8a00fa030ed"; // ───────────────────────── message ───────────────────────── /// drand quicknet is UNCHAINED: message = SHA-256(round as 8-byte big-endian). function messageForRound(uint64 round) internal pure returns (bytes32) { return sha256(abi.encodePacked(round)); } /// Canonical drand randomness derivation: SHA-256 of the compressed signature. function canonicalRandomness(bytes memory compressedSig) internal pure returns (bytes32) { return sha256(compressedSig); } // ─────────────────── RFC 9380 hash-to-field ─────────────────── /** * @dev expand_message_xmd with SHA-256, len_in_bytes = 128 (RFC 9380 §5.3.1), * then reduce the two 64-byte blocks mod p into EIP-2537 Fp elements. * Returns two 64-byte field elements (u0, u1). SHA-256 (0x02) and * MODEXP (0x05) only, so this runs on any EVM (no EIP-2537 needed). */ function hashToField(bytes32 message) internal view returns (bytes memory u0, bytes memory u1) { // msg_prime = Z_pad(64) || msg || l_i_b_str(0x0080) || 0x00 || DST_prime bytes memory zPad = new bytes(64); bytes32 b0 = sha256(abi.encodePacked(zPad, message, uint16(128), uint8(0), DST_PRIME)); bytes32 b1 = sha256(abi.encodePacked(b0, uint8(1), DST_PRIME)); bytes32 b2 = sha256(abi.encodePacked(b0 ^ b1, uint8(2), DST_PRIME)); bytes32 b3 = sha256(abi.encodePacked(b0 ^ b2, uint8(3), DST_PRIME)); bytes32 b4 = sha256(abi.encodePacked(b0 ^ b3, uint8(4), DST_PRIME)); // uniform_bytes = b1 || b2 || b3 || b4 (128 bytes); two 64-byte blocks. u0 = _reduce64ModP(abi.encodePacked(b1, b2)); u1 = _reduce64ModP(abi.encodePacked(b3, b4)); } /** * @dev Full hash-to-curve to G1 (RFC 9380 SSWU_RO). Because EIP-2537's * MAP_FP_TO_G1 already clears the cofactor, and cofactor clearing is a * group homomorphism, map(u0) + map(u1) equals RFC 9380's * clear_cofactor(map(u0) + map(u1)). Requires EIP-2537. * @return point 128-byte uncompressed G1 point, ok false if any precompile * is absent or misbehaves (fail-closed). */ function hashToG1(bytes32 message) internal view returns (bytes memory point, bool ok) { (bytes memory u0, bytes memory u1) = hashToField(message); (bytes memory p0, bool ok0) = _mapFpToG1(u0); (bytes memory p1, bool ok1) = _mapFpToG1(u1); if (!ok0 || !ok1) return (new bytes(0), false); (bool s, bytes memory out) = BLS_G1ADD.staticcall(abi.encodePacked(p0, p1)); if (!s || out.length != 128) return (new bytes(0), false); return (out, true); } // ─────────────────── G1 signature decompression ─────────────────── /** * @dev Decompress a 48-byte compressed G1 point (zcash/ETH2 serialization, * which drand uses) into a 128-byte EIP-2537 uncompressed point. * Uses only MODEXP, so it runs without EIP-2537. Fail-closed: returns * ok=false for the point at infinity, non-compressed encodings, * non-canonical x (>= p), or an x whose x^3+4 is not a square. */ function decompressG1(bytes memory comp) internal view returns (bytes memory point, bool ok) { if (comp.length != 48) return (new bytes(0), false); uint8 flags = uint8(comp[0]); if (flags & 0x80 == 0) return (new bytes(0), false); // must be compressed if (flags & 0x40 != 0) return (new bytes(0), false); // infinity disallowed bool signBit = (flags & 0x20) != 0; bytes memory x = new bytes(48); x[0] = bytes1(flags & 0x1f); // clear the 3 metadata bits for (uint256 i = 1; i < 48; i++) { x[i] = comp[i]; } if (_ge(x, FIELD_MODULUS)) return (new bytes(0), false); // x must be < p // y^2 = x^3 + 4 (mod p) bytes memory y2 = _addSmallModP(_modexp(x, hex"03", FIELD_MODULUS), 4); // candidate y = (y^2)^((p+1)/4) (mod p) bytes memory y = _modexp(y2, SQRT_EXP, FIELD_MODULUS); // reject if not a genuine square root (point not on curve) if (keccak256(_modexp(y, hex"02", FIELD_MODULUS)) != keccak256(y2)) { return (new bytes(0), false); } // pick the root whose "sign" matches the compression flag bytes memory negY = _sub(FIELD_MODULUS, y); // p - y bool yIsLarger = _ge(y, negY); bytes memory yFinal; if (signBit) { yFinal = yIsLarger ? y : negY; } else { yFinal = yIsLarger ? negY : y; } point = abi.encodePacked(bytes16(0), x, bytes16(0), yFinal); // 16+48+16+48 = 128 ok = true; } // ───────────────────────── verify ───────────────────────── /** * @notice Verify a drand quicknet beacon round from its compressed signature. * Fail-closed end to end. Requires EIP-2537 for the pairing; without * it, hashToG1 / pairing produce empty output and this returns false. */ function verifyRoundCompressed(uint64 round, bytes memory compressedSig) internal view returns (bool) { (bytes memory sigPoint, bool sigOk) = decompressG1(compressedSig); if (!sigOk) return false; return verifyRoundUncompressed(round, sigPoint); } /** * @notice Verify with an already-uncompressed 128-byte G1 signature point. * Checks e(H(m), pubkey) * e(sig, -G2) == 1 via EIP-2537 pairing. */ function verifyRoundUncompressed(uint64 round, bytes memory sigPoint) internal view returns (bool) { if (sigPoint.length != 128) return false; (bytes memory hm, bool ok) = hashToG1(messageForRound(round)); if (!ok) return false; // pairing input: (H(m), pubkey) then (sig, -G2_generator) = 2 * 384 bytes bytes memory input = abi.encodePacked(hm, PUBKEY_G2, sigPoint, NEG_G2_GENERATOR); (bool s, bytes memory out) = BLS_PAIRING.staticcall(input); if (!s || out.length != 32) return false; return out[31] == 0x01 && _isOne(out); } // ───────────────────────── helpers ───────────────────────── function _mapFpToG1(bytes memory fp64) private view returns (bytes memory pt, bool ok) { (bool s, bytes memory out) = BLS_MAP_FP_G1.staticcall(fp64); if (s && out.length == 128) return (out, true); return (new bytes(0), false); } function _modexp(bytes memory base, bytes memory exp, bytes memory mod) private view returns (bytes memory) { bytes memory input = abi.encodePacked( uint256(base.length), uint256(exp.length), uint256(mod.length), base, exp, mod ); (bool s, bytes memory out) = MODEXP.staticcall(input); require(s && out.length == mod.length, "AereDrand: MODEXP failed"); return out; } /// Reduce a 64-byte big-endian value mod p, returning a 64-byte EIP-2537 Fp element. function _reduce64ModP(bytes memory chunk64) private view returns (bytes memory) { bytes memory reduced = _modexp(chunk64, hex"01", FIELD_MODULUS); // 48 bytes, < p return abi.encodePacked(bytes16(0), reduced); // left-pad to 64 } /// a >= b for equal-length big-endian byte arrays. function _ge(bytes memory a, bytes memory b) private pure returns (bool) { for (uint256 i = 0; i < a.length; i++) { if (a[i] != b[i]) return uint8(a[i]) > uint8(b[i]); } return true; // equal } /// a - b for equal-length big-endian arrays, assuming a >= b. function _sub(bytes memory a, bytes memory b) private pure returns (bytes memory) { bytes memory r = new bytes(a.length); int256 borrow = 0; for (uint256 i = a.length; i > 0; i--) { int256 d = int256(uint256(uint8(a[i - 1]))) - int256(uint256(uint8(b[i - 1]))) - borrow; if (d < 0) { d += 256; borrow = 1; } else { borrow = 0; } r[i - 1] = bytes1(uint8(uint256(d))); } return r; } /// (t + c) mod p for a small c, where t is a 48-byte value < p. function _addSmallModP(bytes memory t, uint256 c) private pure returns (bytes memory) { bytes memory r = new bytes(t.length); for (uint256 i = 0; i < t.length; i++) { r[i] = t[i]; } uint256 carry = c; for (uint256 i = t.length; i > 0 && carry > 0; i--) { uint256 v = uint256(uint8(r[i - 1])) + (carry & 0xff); r[i - 1] = bytes1(uint8(v & 0xff)); carry = (carry >> 8) + (v >> 8); } if (_ge(r, FIELD_MODULUS)) { r = _sub(r, FIELD_MODULUS); } return r; } function _isOne(bytes memory out) private pure returns (bool) { for (uint256 i = 0; i < 31; i++) { if (out[i] != 0x00) return false; } return out[31] == 0x01; } } /** * @title AereRandomnessBeaconV2 * @notice Fail-closed, self-verifying drand quicknet randomness beacon for Aere. * * This is the honest replacement for AereRandomnessBeacon's stub verifier. * `submitRound` ALWAYS performs a real on-chain BLS12-381 pairing check * against the drand quicknet public key before storing anything. There is no * "phase 1 trust the submitter" mode and no admin switch that turns * verification off: a round that does not verify cannot be stored, and the * stored randomness is the canonical drand value SHA-256(signature). * * This contract is a BUILT-AND-TESTED replacement. It is NOT a live swap of * the deployed beacon; activation on Aere mainnet is founder-gated. * * Quantum note: drand BLS is classical (Shor-breakable). Verifying it does * not make the randomness post-quantum. This is a correctness/honesty fix. */ contract AereRandomnessBeaconV2 { using AereDrandQuicknetBLS for *; bytes32 public constant DRAND_CHAIN_HASH = 0x52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971; uint64 public constant DRAND_PERIOD = 3; uint64 public constant DRAND_GENESIS = 1692803367; mapping(uint64 => bytes32) public randomnessOfRound; mapping(uint64 => bytes) public signatureOfRound; mapping(uint64 => address) public submitterOfRound; event RoundSubmitted(uint64 indexed round, bytes32 randomness, address indexed submitter); /** * @notice Submit and verify a drand quicknet round. Anyone may call; the * submitter pays gas. Reverts (fail-closed) if the BLS signature * does not verify against the quicknet public key. * @param round drand round number. * @param signature 48-byte compressed G1 drand signature (as published at api.drand.sh). * @return randomness canonical drand randomness = SHA-256(signature). */ function submitRound(uint64 round, bytes calldata signature) external returns (bytes32 randomness) { require(round > 0, "AereDrand: bad round"); require(signature.length == 48, "AereDrand: sig must be 48 bytes (G1 compressed)"); require(randomnessOfRound[round] == bytes32(0), "AereDrand: already submitted"); uint64 roundTime = DRAND_GENESIS + (round * DRAND_PERIOD); require(roundTime <= block.timestamp + 30, "AereDrand: round not yet emitted by drand"); // MANDATORY, fail-closed on-chain BLS12-381 verification. require( AereDrandQuicknetBLS.verifyRoundCompressed(round, signature), "AereDrand: BLS verification failed" ); randomness = AereDrandQuicknetBLS.canonicalRandomness(signature); randomnessOfRound[round] = randomness; signatureOfRound[round] = signature; submitterOfRound[round] = msg.sender; emit RoundSubmitted(round, randomness, msg.sender); } function getRandomness(uint64 round) external view returns (bytes32) { return randomnessOfRound[round]; } function isAvailable(uint64 round) external view returns (bool) { return randomnessOfRound[round] != bytes32(0); } function roundAtTime(uint64 unixTimestamp) external pure returns (uint64) { require(unixTimestamp >= DRAND_GENESIS, "AereDrand: before drand genesis"); return uint64((unixTimestamp - DRAND_GENESIS + DRAND_PERIOD - 1) / DRAND_PERIOD); } // ───────── transparency / test surface (pure crypto, safe to expose) ───────── /// The BLS message that is signed for `round` (SHA-256 of the 8-byte round). function messageForRound(uint64 round) external pure returns (bytes32) { return AereDrandQuicknetBLS.messageForRound(round); } /// RFC 9380 hash-to-field of a round's message: two 64-byte EIP-2537 Fp elements. function hashToFieldForRound(uint64 round) external view returns (bytes memory u0, bytes memory u1) { return AereDrandQuicknetBLS.hashToField(AereDrandQuicknetBLS.messageForRound(round)); } /// Full RFC 9380 hash-to-curve of a round's message to G1 (needs EIP-2537). function hashToG1ForRound(uint64 round) external view returns (bytes memory point, bool ok) { return AereDrandQuicknetBLS.hashToG1(AereDrandQuicknetBLS.messageForRound(round)); } /// Decompress a 48-byte compressed G1 signature to a 128-byte point. function decompressSignature(bytes calldata compressedSig) external view returns (bytes memory point, bool ok) { return AereDrandQuicknetBLS.decompressG1(compressedSig); } /// Canonical drand randomness for a compressed signature (SHA-256). function canonicalRandomness(bytes calldata compressedSig) external pure returns (bytes32) { return AereDrandQuicknetBLS.canonicalRandomness(compressedSig); } /// Read-only verify (does not store). Fail-closed; requires EIP-2537. function verifyRound(uint64 round, bytes calldata compressedSig) external view returns (bool) { return AereDrandQuicknetBLS.verifyRoundCompressed(round, compressedSig); } }