// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; /** * @title AereRandomnessBeacon * @notice Permissionless drand verifiable randomness beacon for AERE Network. * * drand (https://drand.love) is the distributed randomness beacon operated * by the League of Entropy — Cloudflare, EPFL, Protocol Labs, U.S. Naval * Research Lab, and ~15 other independent organisations. Every 3 seconds * they collectively sign a fresh random value via threshold BLS12-381. * Anyone can fetch the current and historical rounds for free at * https://api.drand.sh — and anyone can submit any round to this contract * to make it on-chain readable. * * Phase 1 (this deployment): randomness is submitted by anyone, paid by the * submitter, with off-chain auditability — the canonical drand signature * for any round is public and verifiable at api.drand.sh. The contract * stores randomness keyed by drand round number and emits the submitter * address in every event so the chain has a public submitter ledger. * * Phase 2 (after audit): swap to on-chain BLS pairing verification using * AERE's EIP-2537 precompiles (activated in Pectra, Tier 1.1). The * submission interface is forward-compatible — Phase 2 just enables the * pairing check inside `_verify()`. * * Zero Foundation cost: no relayer to run, no subscription, no validators * to coordinate. Consumers (dApps that want a random number) submit the * round they need when they need it. Permissionless. * * Reference drand chain (Quicknet, bls-unchained-g1-rfc9380): * Chain hash: 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 * Period: 3 seconds · Genesis: 1692803367 * Public key (G2, compressed): 0x83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a */ contract AereRandomnessBeacon is Ownable, ReentrancyGuard { /// drand Quicknet chain hash (the chain we currently consume from). bytes32 public constant DRAND_CHAIN_HASH = 0x52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971; /// drand Quicknet period (seconds between rounds). uint64 public constant DRAND_PERIOD = 3; /// drand Quicknet genesis unix timestamp. uint64 public constant DRAND_GENESIS = 1692803367; /// drand Quicknet aggregated public key (G2, compressed). 96 bytes. bytes public DRAND_PUBKEY = hex"83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"; /// Phase flag: true = on-chain BLS pairing verification required (Phase 2); /// false = off-chain auditability only (Phase 1). bool public strictVerifyEnabled = false; /// Per-round storage. Once written, immutable. 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); event StrictVerifyChanged(bool enabled); event DrandPubkeyChanged(bytes newPubkey); // ───────────────────── Admin ───────────────────── /// Foundation enables strict on-chain BLS pairing verification in Phase 2. function setStrictVerify(bool _enabled) external onlyOwner { strictVerifyEnabled = _enabled; emit StrictVerifyChanged(_enabled); } /// Allow Foundation to rotate to a new drand chain (e.g. if Quicknet is succeeded by a faster chain). function setDrandPubkey(bytes calldata _pubkey) external onlyOwner { DRAND_PUBKEY = _pubkey; emit DrandPubkeyChanged(_pubkey); } // ───────────────────── Submit ───────────────────── /** * @notice Submit a drand round. Anyone can call. Submitter pays the gas. * @param round Drand round number (monotonic, 1 = first round after genesis). * @param signature drand BLS signature for this round, fetched from api.drand.sh. * Format: 48-byte compressed G1 point (drand Quicknet uses G1 sigs). * @return randomness keccak256(signature) — matches drand's canonical randomness derivation. */ function submitRound(uint64 round, bytes calldata signature) external nonReentrant 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"); // Round must not be in the future of drand's published timeline. uint64 roundTime = DRAND_GENESIS + (round * DRAND_PERIOD); require(roundTime <= block.timestamp + 30, "AereDrand: round not yet emitted by drand"); // Phase 2: on-chain BLS pairing verification. // For now we trust the submitter (Phase 1). The signature can be // verified off-chain against api.drand.sh in O(1). if (strictVerifyEnabled) { require(_verify(round, signature), "AereDrand: BLS verification failed"); } randomness = keccak256(signature); randomnessOfRound[round] = randomness; signatureOfRound[round] = signature; submitterOfRound[round] = msg.sender; emit RoundSubmitted(round, randomness, msg.sender); } /** * @notice Read randomness for a round. Returns bytes32(0) if not yet submitted. */ function getRandomness(uint64 round) external view returns (bytes32) { return randomnessOfRound[round]; } /** * @notice Convenience: get the drand round number that will be emitted at or after a given unix timestamp. */ function roundAtTime(uint64 unixTimestamp) external pure returns (uint64) { require(unixTimestamp >= DRAND_GENESIS, "AereDrand: before drand genesis"); // Round N is emitted at DRAND_GENESIS + N * PERIOD. Ceil-div to get the next round. return uint64((unixTimestamp - DRAND_GENESIS + DRAND_PERIOD - 1) / DRAND_PERIOD); } /// Returns true if randomness has been submitted for this round. function isAvailable(uint64 round) external view returns (bool) { return randomnessOfRound[round] != bytes32(0); } // ───────────────────── Phase 2 verification (placeholder) ───────────────────── /** * @dev Placeholder for Phase 2 on-chain BLS pairing verification using * EIP-2537 precompiles (BLS_PAIRING_CHECK at 0x0f). The full * implementation requires: * 1. Hash the round number to a G1 point following RFC 9380 SSWU_RO_ * (using BLS_MAP_FP_TO_G1 at 0x10 + BLS_G1ADD at 0x0b) * 2. Decompress the 48-byte G1 signature into a 128-byte uncompressed point * 3. Compute pairing check: e(sig, -G2_GEN) * e(msg_G1, DRAND_PK_G2) == 1 * * This is non-trivial Solidity (~300 lines) and deserves its own * audit pass. Wired in Tier 1.7 Phase 2. */ function _verify(uint64 /*round*/, bytes calldata /*signature*/) internal pure returns (bool) { // Phase 2 implementation goes here. return true; } }