aere-contracts/test/AereRandomnessBeaconV2.test.js
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

228 lines
11 KiB
JavaScript

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"
);
});
});
});