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.
228 lines
11 KiB
JavaScript
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"
|
|
);
|
|
});
|
|
});
|
|
});
|