aere-contracts/test/interop/AereEthLightClient.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

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