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.
152 lines
7.6 KiB
Solidity
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;
|
|
}
|
|
}
|