const { expect } = require("chai"); const { ethers } = require("hardhat"); /** * Real drand quicknet beacon verification test. * * Vectors are GENUINE published values from the League of Entropy quicknet chain * (chain hash 52db9ba7...84e971), fetched from https://api.drand.sh: * * /info -> public_key, period 3, genesis 1692803367, * schemeID "bls-unchained-g1-rfc9380" * /public/1000000 -> randomness + signature below * /public/1000001 -> randomness + signature below * * The EIP-2537 pairing assertions require the BLS12-381 precompiles. They are * live on Aere mainnet (Prague parity) and are available in the local EDR EVM * ONLY at the "prague" hardfork. This test detects availability at runtime: * - with EIP-2537: it runs the full on-chain pairing verification (real pass/fail) * - without it: it asserts the contract FAILS CLOSED (never returns true), and * the pairing case is reported as [MEASURE: run under a prague/EIP-2537 EVM]. * * Run the full on-chain verification with: * npx hardhat test test/AereRandomnessBeaconV2.test.js --config hardhat.config.prague.js */ // ── genuine drand quicknet vectors ── const SIG_1000000 = "0x83ad29e4c409f9470fc2ef02f90214df49e02b441a1a241a82d622d9f608ef98fd8b11a029f1bee9d9e83b45088abe72"; const RAND_1000000 = "0xb22aad4794f7451896f7a371aa46106fd84d919f3f569acd5b2fddf1d1440af3"; const SIG_1000001 = "0xa5bd91e5e2d8c0bf51bffdfad87eef34348fd9c0b2df2bee39db90bdef7e1399b1a77bb2fe98b24d84c0936a306c4218"; const RAND_1000001 = "0x9f45f439afd81e9846b3b4dc5e3e6051922c73c8459d18e9d507b52ddbd884ff"; // ── reference constants derived independently with @noble/curves ── const MSG_1000000 = "0xce59b701970051bef0d7efdc1a4196c49ce1bbaaf9c5403626ad7adcc41737e7"; const U0_1000000 = "0x0000000000000000000000000000000016468f66eb172d70cfafac794eddcd7a34c20a0f1509693fbd5e08dc01e5a7676e010f194278e2dc5bb159fdacd30c9d"; const U1_1000000 = "0x000000000000000000000000000000000923cb3c137f776f1a4d1434af4d0d92e90e02940ba28612e87569548e64ab786ef8cc8b970c3cd1c96a19d070eff3b6"; const SIG_1000000_UNCOMPRESSED = "0x0000000000000000000000000000000003ad29e4c409f9470fc2ef02f90214df49e02b441a1a241a82d622d9f608ef98fd8b11a029f1bee9d9e83b45088abe720000000000000000000000000000000001776ff7408b39c5f6f9fa50746efd7eea17fbb61f2e7b9c849ff0528e5a3deeedd029d0df345199963d75ba93b5a02a"; const HM_1000000_G1 = "0x000000000000000000000000000000000533fbfd488393ea4e2fe335e2b768d112934e2477f335d4360c1ed8b96907740b49a16947fe30f7fc76433c70d0940b0000000000000000000000000000000005cf7470bbc5e814f87f58627718949c2ba851c3f1e29220da68829d3502953f3b557c97d72fc76caf341ea244730002"; const lc = (s) => s.toLowerCase(); describe("AereRandomnessBeaconV2 (drand quicknet, real BLS12-381 / EIP-2537)", function () { let beacon; let has2537 = false; before(async function () { const F = await ethers.getContractFactory("AereRandomnessBeaconV2"); beacon = await F.deploy(); await beacon.waitForDeployment(); // Detect EIP-2537 by attempting a real hash-to-curve (needs MAP_FP_TO_G1). const [, ok] = await beacon.hashToG1ForRound(1000000); has2537 = ok; console.log( ` EIP-2537 precompiles available in this EVM: ${has2537 ? "YES (real pairing verification)" : "NO (fail-closed path; pairing = [MEASURE])"}` ); }); // ─────────── construction logic (SHA-256 + MODEXP only; runs on any EVM) ─────────── describe("message + hash-to-field construction (no EIP-2537 needed)", function () { it("reconstructs the unchained quicknet message = SHA-256(round_be8)", async function () { expect(lc(await beacon.messageForRound(1000000))).to.equal(lc(MSG_1000000)); }); it("matches the RFC 9380 hash-to-field vectors (expand_message_xmd SHA-256, DST NUL_)", async function () { const [u0, u1] = await beacon.hashToFieldForRound(1000000); expect(lc(u0)).to.equal(lc(U0_1000000)); expect(lc(u1)).to.equal(lc(U1_1000000)); }); }); describe("G1 signature decompression (MODEXP only; runs on any EVM)", function () { it("decompresses the genuine round-1000000 signature to the reference point", async function () { const [pt, ok] = await beacon.decompressSignature(SIG_1000000); expect(ok).to.equal(true); expect(lc(pt)).to.equal(lc(SIG_1000000_UNCOMPRESSED)); }); it("fails closed on a wrong-length signature", async function () { const [pt, ok] = await beacon.decompressSignature("0x1234"); expect(ok).to.equal(false); expect(pt).to.equal("0x"); }); it("fails closed when x^3+4 is not a square (x not on the curve)", async function () { // x = 0x000000030...0001 is a verified quadratic non-residue for x^3+4 // (found via the Legendre symbol with @noble/curves). Compression flag set // on the leading byte (0x80); after masking, x is the non-residue above. const bad = "0x800000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"; const [pt, ok] = await beacon.decompressSignature(bad); expect(ok).to.equal(false); expect(pt).to.equal("0x"); }); it("fails closed on the point at infinity (infinity flag set)", async function () { // 0xc0 = compression flag (0x80) | infinity flag (0x40) const inf = "0xc0" + "00".repeat(47); const [pt, ok] = await beacon.decompressSignature(inf); expect(ok).to.equal(false); expect(pt).to.equal("0x"); }); }); describe("canonical drand randomness", function () { it("derives randomness = SHA-256(signature), matching api.drand.sh", async function () { expect(lc(await beacon.canonicalRandomness(SIG_1000000))).to.equal(lc(RAND_1000000)); expect(lc(await beacon.canonicalRandomness(SIG_1000001))).to.equal(lc(RAND_1000001)); }); }); describe("submitRound input validation (fail-closed regardless of EIP-2537)", function () { it("rejects round 0", async function () { await expect(beacon.submitRound(0, SIG_1000000)).to.be.revertedWith("AereDrand: bad round"); }); it("rejects a signature that is not 48 bytes", async function () { await expect(beacon.submitRound(1000000, "0x1234")).to.be.revertedWith( "AereDrand: sig must be 48 bytes (G1 compressed)" ); }); }); // ─────────── real on-chain BLS verification (EIP-2537) ─────────── describe("on-chain BLS12-381 pairing verification (EIP-2537)", function () { it("hash-to-curve to G1 matches the independent @noble/curves reference [needs EIP-2537]", async function () { if (!has2537) { console.log(" [MEASURE] skipped: EIP-2537 absent. Run with --config hardhat.config.prague.js"); this.skip(); } const [pt, ok] = await beacon.hashToG1ForRound(1000000); expect(ok).to.equal(true); expect(lc(pt)).to.equal(lc(HM_1000000_G1)); }); it("VERIFIES a genuine round (1000000) as true [needs EIP-2537]", async function () { if (!has2537) { console.log(" [MEASURE] skipped: EIP-2537 absent. Run with --config hardhat.config.prague.js"); this.skip(); } expect(await beacon.verifyRound(1000000, SIG_1000000)).to.equal(true); expect(await beacon.verifyRound(1000001, SIG_1000001)).to.equal(true); }); it("REJECTS a valid signature submitted for the wrong round [needs EIP-2537]", async function () { if (!has2537) { console.log(" [MEASURE] skipped: EIP-2537 absent. Run with --config hardhat.config.prague.js"); this.skip(); } // genuine sig for 1000000, claimed as 1000001, and vice-versa expect(await beacon.verifyRound(1000001, SIG_1000000)).to.equal(false); expect(await beacon.verifyRound(1000000, SIG_1000001)).to.equal(false); }); it("REJECTS a tampered signature [needs EIP-2537]", async function () { if (!has2537) { console.log(" [MEASURE] skipped: EIP-2537 absent. Run with --config hardhat.config.prague.js"); this.skip(); } // flip one byte of the genuine signature const bytes = ethers.getBytes(SIG_1000000); bytes[20] ^= 0x01; const tampered = ethers.hexlify(bytes); expect(await beacon.verifyRound(1000000, tampered)).to.equal(false); }); }); describe("submitRound end-to-end", function () { it("stores canonical randomness for a genuine round, and rejects double-submit [needs EIP-2537]", async function () { if (!has2537) { console.log(" [MEASURE] skipped: EIP-2537 absent. Run with --config hardhat.config.prague.js"); this.skip(); } const F = await ethers.getContractFactory("AereRandomnessBeaconV2"); const b = await F.deploy(); await b.waitForDeployment(); await expect(b.submitRound(1000000, SIG_1000000)) .to.emit(b, "RoundSubmitted"); expect(lc(await b.getRandomness(1000000))).to.equal(lc(RAND_1000000)); expect(await b.isAvailable(1000000)).to.equal(true); await expect(b.submitRound(1000000, SIG_1000000)).to.be.revertedWith( "AereDrand: already submitted" ); }); it("reverts (fail-closed) when submitting a signature that does not verify", async function () { const F = await ethers.getContractFactory("AereRandomnessBeaconV2"); const b = await F.deploy(); await b.waitForDeployment(); // On a NON-EIP-2537 EVM this reverts because verification fails closed // (precompiles absent). On an EIP-2537 EVM it reverts because the sig for // round 1000001 does not verify against round 1000000's message. Either // way: no storage, never accepted as true. await expect(b.submitRound(1000000, SIG_1000001)).to.be.revertedWith( "AereDrand: BLS verification failed" ); expect(await b.isAvailable(1000000)).to.equal(false); }); it("fail-closed check: without EIP-2537, a GENUINE sig is also rejected (never stub-true)", async function () { if (has2537) { console.log(" (EIP-2537 present: this fail-closed-absence check is not applicable)"); this.skip(); } const F = await ethers.getContractFactory("AereRandomnessBeaconV2"); const b = await F.deploy(); await b.waitForDeployment(); // The old stub returned true here. The real verifier must return false // when it cannot actually check the pairing. expect(await b.verifyRound(1000000, SIG_1000000)).to.equal(false); await expect(b.submitRound(1000000, SIG_1000000)).to.be.revertedWith( "AereDrand: BLS verification failed" ); }); }); });