aere-contracts/contracts/oracle/AereRandomnessBeacon.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

152 lines
7.6 KiB
Solidity

// 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;
}
}