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.
247 lines
10 KiB
JavaScript
247 lines
10 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
const H = require("./helpers");
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// AereEthLightClient — verified against AERE's LIVE EIP-2537 BLS12-381 precompiles
|
|
// under the Prague hardfork (real crypto, NOT a mock). SSZ roots and Merkle branches
|
|
// are cross-checked between the Solidity contract and an independent JS implementation.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// A light-client update verifies ~512 pubkeys + BLS aggregation on precompiles;
|
|
// measured ~32M gas (rotation ~2x). AERE's block gas is effectively unlimited; the
|
|
// test just needs an explicit limit above hardhat's ~30M auto-send default.
|
|
const GAS = { gasLimit: 400_000_000 };
|
|
|
|
const GVR = "0x" + "11".repeat(32); // synthetic genesis_validators_root
|
|
const FORK_VERSION = "0x02000000"; // synthetic fork version (Bellatrix-shaped)
|
|
const DOMAIN_SYNC = "0x07000000";
|
|
|
|
// Generalized indices (consensus-spec): finalized_checkpoint.root=105 (depth6),
|
|
// next_sync_committee=55 (depth5).
|
|
const GI_FINALIZED = 105;
|
|
const GI_NEXT = 55;
|
|
|
|
function beaconHeaderStruct(h) {
|
|
return {
|
|
slot: h.slot,
|
|
proposerIndex: h.proposerIndex,
|
|
parentRoot: H.hex(h.parentRoot),
|
|
stateRoot: H.hex(h.stateRoot),
|
|
bodyRoot: H.hex(h.bodyRoot),
|
|
};
|
|
}
|
|
|
|
// Build a complete, internally-consistent light-client update.
|
|
// opts: { committee, finalizedSlot, attestedSlot, participants, nextCommittee }
|
|
function buildUpdate(committee, opts) {
|
|
const finalizedHeader = {
|
|
slot: opts.finalizedSlot,
|
|
proposerIndex: 7,
|
|
parentRoot: Buffer.from("aa".repeat(32), "hex"),
|
|
stateRoot: Buffer.from("bb".repeat(32), "hex"),
|
|
bodyRoot: opts.finalizedBodyRoot || Buffer.from("cc".repeat(32), "hex"),
|
|
};
|
|
const finalizedRoot = H.beaconHeaderRoot(finalizedHeader);
|
|
|
|
// Assign leaves in the attested state tree (depth 6 covers gindex 55 and 105).
|
|
const assign = {};
|
|
assign[GI_FINALIZED] = finalizedRoot;
|
|
if (opts.nextCommittee) assign[GI_NEXT] = opts.nextCommittee.root;
|
|
const tree = H.buildTree(6, assign);
|
|
|
|
const attestedHeader = {
|
|
slot: opts.attestedSlot,
|
|
proposerIndex: 9,
|
|
parentRoot: Buffer.from("dd".repeat(32), "hex"),
|
|
stateRoot: tree.root, // commits both finality and (optionally) next committee
|
|
bodyRoot: Buffer.from("ee".repeat(32), "hex"),
|
|
};
|
|
const attestedRoot = H.beaconHeaderRoot(attestedHeader);
|
|
const domain = H.computeDomain(
|
|
Buffer.from(DOMAIN_SYNC.slice(2), "hex"),
|
|
Buffer.from(FORK_VERSION.slice(2), "hex"),
|
|
Buffer.from(GVR.slice(2), "hex")
|
|
);
|
|
const signingRoot = H.shaPair(attestedRoot, domain);
|
|
|
|
const { bits, aggSigUncompressed } = H.signSubset(committee, signingRoot, opts.participants);
|
|
|
|
const finalityBranch = tree.branch(GI_FINALIZED).map(H.hex);
|
|
const nextBranch = opts.nextCommittee ? tree.branch(GI_NEXT).map(H.hex) : [];
|
|
|
|
return {
|
|
attestedHeader: beaconHeaderStruct(attestedHeader),
|
|
finalizedHeader: beaconHeaderStruct(finalizedHeader),
|
|
finalityBranch,
|
|
participationBits: H.hex(bits),
|
|
signature: H.hex(aggSigUncompressed),
|
|
pubkeys: H.hex(H.cat(...committee.uncompressed)),
|
|
aggregatePubkey: H.hex(committee.aggUncompressed),
|
|
signatureForkVersion: FORK_VERSION,
|
|
signedByNextCommittee: opts.signedByNextCommittee || false,
|
|
hasNextCommittee: !!opts.nextCommittee,
|
|
nextPubkeys: opts.nextCommittee ? H.hex(H.cat(...opts.nextCommittee.uncompressed)) : "0x",
|
|
nextAggregatePubkey: opts.nextCommittee ? H.hex(opts.nextCommittee.aggUncompressed) : "0x",
|
|
nextSyncCommitteeBranch: nextBranch,
|
|
_finalizedRoot: finalizedRoot,
|
|
_signingRoot: signingRoot,
|
|
_attestedRoot: attestedRoot,
|
|
_domain: domain,
|
|
};
|
|
}
|
|
|
|
describe("AereEthLightClient (real EIP-2537 BLS, Prague)", function () {
|
|
let committee, harness, bootstrapFinalized;
|
|
|
|
before(async function () {
|
|
this.timeout(300000);
|
|
committee = H.makeCommittee("aere-eth-lc-committee-1");
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
bootstrapFinalized = {
|
|
slot: 1000,
|
|
proposerIndex: 1,
|
|
parentRoot: "0x" + "00".repeat(32),
|
|
stateRoot: "0x" + "01".repeat(32),
|
|
bodyRoot: "0x" + "02".repeat(32),
|
|
};
|
|
const F = await ethers.getContractFactory("LightClientHarness");
|
|
harness = await F.deploy(H.hex(H.DST), GVR, H.hex(committee.root), bootstrapFinalized);
|
|
await harness.waitForDeployment();
|
|
});
|
|
|
|
describe("cross-checks: Solidity vs independent JS reference", function () {
|
|
it("computes the SyncCommittee SSZ root identically", async function () {
|
|
const onchain = await harness.h_committeeRoot(
|
|
H.hex(H.cat(...committee.uncompressed)),
|
|
H.hex(committee.aggUncompressed)
|
|
);
|
|
expect(onchain).to.equal(H.hex(committee.root));
|
|
});
|
|
|
|
it("compresses a G1 pubkey identically to noble", async function () {
|
|
const onchain = await harness.h_compress(H.hex(committee.uncompressed[0]));
|
|
expect(onchain).to.equal(H.hex(committee.compressed[0]));
|
|
});
|
|
|
|
it("computes the sync-committee domain identically", async function () {
|
|
const onchain = await harness.h_domain(FORK_VERSION);
|
|
const expected = H.computeDomain(
|
|
Buffer.from(DOMAIN_SYNC.slice(2), "hex"),
|
|
Buffer.from(FORK_VERSION.slice(2), "hex"),
|
|
Buffer.from(GVR.slice(2), "hex")
|
|
);
|
|
expect(onchain).to.equal(H.hex(expected));
|
|
});
|
|
|
|
it("hashes a message to G2 identically to noble hashToCurve", async function () {
|
|
const msg = Buffer.from("interop-hash-to-curve-check", "ascii");
|
|
const onchain = await harness.h_hashToG2(H.hex(msg));
|
|
const expected = H.encG2(H.bls.G2.hashToCurve(msg));
|
|
expect(onchain.toLowerCase()).to.equal(H.hex(expected).toLowerCase());
|
|
});
|
|
});
|
|
|
|
describe("processUpdate — happy path", function () {
|
|
it("accepts a valid full-participation update and advances finality", async function () {
|
|
this.timeout(300000);
|
|
const participants = [...Array(H.COMMITTEE_SIZE).keys()];
|
|
const u = buildUpdate(committee, {
|
|
finalizedSlot: 2000,
|
|
attestedSlot: 2032,
|
|
participants,
|
|
});
|
|
await expect(harness.processUpdate(u, GAS)).to.emit(harness, "FinalityUpdated");
|
|
expect(await harness.finalizedSlot()).to.equal(2000n);
|
|
expect(await harness.finalizedHeaderRoot()).to.equal(H.hex(u._finalizedRoot));
|
|
});
|
|
|
|
it("accepts exactly the 2/3 participation threshold (342 of 512)", async function () {
|
|
this.timeout(300000);
|
|
const participants = [...Array(342).keys()];
|
|
const u = buildUpdate(committee, { finalizedSlot: 2100, attestedSlot: 2132, participants });
|
|
await expect(harness.processUpdate(u, GAS)).to.emit(harness, "FinalityUpdated");
|
|
expect(await harness.finalizedSlot()).to.equal(2100n);
|
|
});
|
|
});
|
|
|
|
describe("processUpdate — rejections", function () {
|
|
it("rejects below-threshold participation (341 of 512)", async function () {
|
|
this.timeout(300000);
|
|
const participants = [...Array(341).keys()];
|
|
const u = buildUpdate(committee, { finalizedSlot: 2200, attestedSlot: 2232, participants });
|
|
await expect(harness.processUpdate(u, GAS)).to.be.revertedWith("LC:insufficient participation");
|
|
});
|
|
|
|
it("rejects a forged signature (valid participants, tampered signature)", async function () {
|
|
this.timeout(300000);
|
|
const participants = [...Array(400).keys()];
|
|
const u = buildUpdate(committee, { finalizedSlot: 2300, attestedSlot: 2332, participants });
|
|
// Flip the signature to a different valid G2 point (sig of a different message).
|
|
const other = H.signSubset(committee, Buffer.from("99".repeat(32), "hex"), participants);
|
|
u.signature = H.hex(other.aggSigUncompressed);
|
|
await expect(harness.processUpdate(u, GAS)).to.be.revertedWith("LC:bad signature");
|
|
});
|
|
|
|
it("rejects a committee whose pubkeys don't hash to the committed root", async function () {
|
|
this.timeout(300000);
|
|
const wrong = H.makeCommittee("a-different-committee");
|
|
const participants = [...Array(400).keys()];
|
|
const u = buildUpdate(committee, { finalizedSlot: 2400, attestedSlot: 2432, participants });
|
|
// swap in the wrong committee's pubkeys (signature no longer matters; root check first)
|
|
u.pubkeys = H.hex(H.cat(...wrong.uncompressed));
|
|
u.aggregatePubkey = H.hex(wrong.aggUncompressed);
|
|
await expect(harness.processUpdate(u, GAS)).to.be.revertedWith("LC:committee mismatch");
|
|
});
|
|
|
|
it("rejects a tampered finality branch", async function () {
|
|
this.timeout(300000);
|
|
const participants = [...Array(400).keys()];
|
|
const u = buildUpdate(committee, { finalizedSlot: 2500, attestedSlot: 2532, participants });
|
|
const b = u.finalityBranch.slice();
|
|
b[0] = "0x" + "de".repeat(32);
|
|
u.finalityBranch = b;
|
|
await expect(harness.processUpdate(u, GAS)).to.be.revertedWith("LC:bad finality branch");
|
|
});
|
|
|
|
it("rejects stale finality (slot not advancing)", async function () {
|
|
this.timeout(300000);
|
|
const participants = [...Array(400).keys()];
|
|
const u = buildUpdate(committee, { finalizedSlot: 900, attestedSlot: 2000, participants });
|
|
await expect(harness.processUpdate(u, GAS)).to.be.revertedWith("LC:stale finality");
|
|
});
|
|
});
|
|
|
|
describe("committee rotation", function () {
|
|
it("stores a next committee, then rotates to it on a next-committee-signed update", async function () {
|
|
this.timeout(300000);
|
|
const next = H.makeCommittee("aere-eth-lc-committee-2");
|
|
const participants = [...Array(400).keys()];
|
|
|
|
// Update 1: signed by current committee, carries next_sync_committee.
|
|
const u1 = buildUpdate(committee, {
|
|
finalizedSlot: 3000,
|
|
attestedSlot: 3032,
|
|
participants,
|
|
nextCommittee: next,
|
|
});
|
|
await expect(harness.processUpdate(u1, GAS)).to.emit(harness, "NextCommitteeStored");
|
|
expect(await harness.nextSyncCommitteeRoot()).to.equal(H.hex(next.root));
|
|
|
|
// Update 2: signed by the NEXT committee -> current rotates to next.
|
|
const u2 = buildUpdate(next, {
|
|
finalizedSlot: 4000,
|
|
attestedSlot: 4032,
|
|
participants,
|
|
signedByNextCommittee: true,
|
|
});
|
|
await expect(harness.processUpdate(u2, GAS)).to.emit(harness, "CommitteeRotated");
|
|
expect(await harness.currentSyncCommitteeRoot()).to.equal(H.hex(next.root));
|
|
expect(await harness.nextSyncCommitteeRoot()).to.equal("0x" + "00".repeat(32));
|
|
expect(await harness.finalizedSlot()).to.equal(4000n);
|
|
});
|
|
});
|
|
});
|