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

182 lines
8.2 KiB
JavaScript

// AereZkQbftLightClient — the OUTBOUND zk light client: an external verifier advances a
// trust-minimized view of AERE's OWN QBFT finality by checking an SP1 Groth16 proof that one
// chain-2800 block is final (>= 2f+1 valid ECDSA committed seals from the trusted validator set).
//
// The SP1 verifier is MOCKED (MockSp1Verifier: verifyProof reverts unless the exact
// (vkey, publicValues, proof) triple was pre-registered — mirroring the real gateway, which
// reverts on an invalid proof). So these tests exercise the CONTRACT's job: decode the guest's
// PublicValues byte-for-byte, bind them to the trusted validator-set anchor, enforce a strictly
// advancing block number + finalized flag, and record the new head. The finality math itself is
// proven separately in `zk-light-client/qbft-finality/tests/real_headers.rs` against real headers.
const { expect } = require("chai");
const { ethers } = require("hardhat");
const VKEY = "0x" + "ab".repeat(32);
const WRONG_VKEY = "0x" + "cd".repeat(32);
const PROOF = "0x1234567890"; // opaque proof bytes; the mock keys acceptance on the triple
// EXACTLY the guest's PublicValues struct order (sol! { bytes32 blockHash; uint64 blockNumber;
// bytes32 committedSealHash; bytes32 validatorSetRoot; bool finalized; bool falconVerified; }).
const PV_TYPES = ["bytes32", "uint64", "bytes32", "bytes32", "bool", "bool"];
function encodePV({ blockHash, blockNumber, committedSealHash, validatorSetRoot, finalized, falconVerified }) {
return ethers.AbiCoder.defaultAbiCoder().encode(PV_TYPES, [
blockHash, blockNumber, committedSealHash, validatorSetRoot, finalized, falconVerified,
]);
}
function marker(vkey, pv, proof) {
return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes32", "bytes", "bytes"], [vkey, pv, proof]));
}
// validator_set_root exactly as the guest: keccak256(addr_0 ‖ addr_1 ‖ … ) in stored order.
function validatorSetRoot(addrs) {
return ethers.keccak256(ethers.concat(addrs.map((a) => ethers.getBytes(a))));
}
const VALIDATORS = [
"0x6ef891a60bdb5907f8e4cc539fb818edbdfec552",
"0x93fbd0a959e976d8d91bad36e250487f2da69c69",
"0x42405515f7d8f85dcb2183fef5d6b980c64fc417",
];
const VSROOT = validatorSetRoot(VALIDATORS);
const BOOT_NUM = 9241600n;
const BOOT_HASH = "0x" + "11".repeat(32);
describe("AereZkQbftLightClient (outbound zk light client of AERE QBFT finality)", function () {
let mock, lc, prover;
function pv(over = {}) {
return encodePV({
blockHash: "0x" + "22".repeat(32),
blockNumber: 9241622n,
committedSealHash: "0x" + "33".repeat(32),
validatorSetRoot: VSROOT,
finalized: true,
falconVerified: false,
...over,
});
}
async function accept(publicValues, proof = PROOF) {
await mock.setValid(marker(VKEY, publicValues, proof), true);
}
beforeEach(async function () {
[prover] = await ethers.getSigners();
mock = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
await mock.waitForDeployment();
lc = await (await ethers.getContractFactory("AereZkQbftLightClient")).deploy(
await mock.getAddress(), VKEY, VSROOT, BOOT_NUM, BOOT_HASH
);
await lc.waitForDeployment();
});
it("bootstraps the anchor + initial head immutably", async function () {
expect(await lc.SP1_VERIFIER()).to.equal(await mock.getAddress());
expect(await lc.PROGRAM_VKEY()).to.equal(VKEY);
expect(await lc.trustedValidatorSetRoot()).to.equal(VSROOT);
expect(await lc.finalizedNumber()).to.equal(BOOT_NUM);
expect(await lc.finalizedBlockHash()).to.equal(BOOT_HASH);
expect(await lc.updateCount()).to.equal(0n);
});
it("the guest PublicValues ABI-encoding is exactly 192 bytes (6 x 32)", async function () {
expect(ethers.getBytes(pv()).length).to.equal(192);
});
it("advances the tracked finalized head on a valid proof + emits FinalityAdvanced", async function () {
const p = pv();
await accept(p);
await expect(lc.processProof(p, PROOF))
.to.emit(lc, "FinalityAdvanced")
.withArgs(9241622n, "0x" + "22".repeat(32), "0x" + "33".repeat(32), false, prover.address);
expect(await lc.finalizedNumber()).to.equal(9241622n);
expect(await lc.finalizedBlockHash()).to.equal("0x" + "22".repeat(32));
expect(await lc.lastCommittedSealHash()).to.equal("0x" + "33".repeat(32));
expect(await lc.lastFalconVerified()).to.equal(false);
expect(await lc.updateCount()).to.equal(1n);
});
it("advances again for a strictly higher block number", async function () {
const p1 = pv({ blockNumber: 9241622n });
await accept(p1); await lc.processProof(p1, PROOF);
const p2 = pv({ blockNumber: 9241650n, blockHash: "0x" + "44".repeat(32) });
await accept(p2); await lc.processProof(p2, PROOF);
expect(await lc.finalizedNumber()).to.equal(9241650n);
expect(await lc.finalizedBlockHash()).to.equal("0x" + "44".repeat(32));
expect(await lc.updateCount()).to.equal(2n);
});
it("rejects a proof whose validator-set root != the trusted anchor", async function () {
const bad = pv({ validatorSetRoot: "0x" + "ff".repeat(32) });
await accept(bad);
await expect(lc.processProof(bad, PROOF)).to.be.revertedWithCustomError(lc, "ValidatorSetMismatch");
});
it("rejects a non-advancing block number (equal to current head)", async function () {
const eq = pv({ blockNumber: BOOT_NUM });
await accept(eq);
await expect(lc.processProof(eq, PROOF)).to.be.revertedWithCustomError(lc, "StaleFinality");
});
it("rejects a lower block number after advancing", async function () {
const p = pv({ blockNumber: 9241650n });
await accept(p); await lc.processProof(p, PROOF);
const lower = pv({ blockNumber: 9241640n, blockHash: "0x" + "55".repeat(32) });
await accept(lower);
await expect(lc.processProof(lower, PROOF)).to.be.revertedWithCustomError(lc, "StaleFinality");
});
it("rejects finalized=false (guest would never emit it, but the contract re-checks)", async function () {
const nf = pv({ finalized: false });
await accept(nf);
await expect(lc.processProof(nf, PROOF)).to.be.revertedWithCustomError(lc, "NotFinalized");
});
it("rejects a zero block hash", async function () {
const zh = pv({ blockHash: ethers.ZeroHash });
await accept(zh);
await expect(lc.processProof(zh, PROOF)).to.be.revertedWithCustomError(lc, "ZeroBlockHash");
});
it("rejects public values of the wrong length", async function () {
const short = "0x" + "00".repeat(160); // 160 != 192
await mock.setValid(marker(VKEY, short, PROOF), true);
await expect(lc.processProof(short, PROOF)).to.be.revertedWithCustomError(lc, "BadPublicValuesLength");
});
it("rejects an invalid proof (mock not registered => gateway reverts)", async function () {
const p = pv(); // not registered with the mock
await expect(lc.processProof(p, PROOF)).to.be.revertedWithCustomError(lc, "InvalidProof");
});
it("rejects a proof registered under the WRONG vkey", async function () {
const p = pv();
await mock.setValid(marker(WRONG_VKEY, p, PROOF), true); // right pv, wrong program
await expect(lc.processProof(p, PROOF)).to.be.revertedWithCustomError(lc, "InvalidProof");
});
it("records falconVerified when the reserved PQC slot is set (forward-compat)", async function () {
const p = pv({ falconVerified: true });
await accept(p);
await lc.processProof(p, PROOF);
expect(await lc.lastFalconVerified()).to.equal(true);
});
it("verify() is stateless: returns decoded fields, records nothing", async function () {
const p = pv();
await accept(p);
const res = await lc.verify.staticCall(p, PROOF);
expect(res[0]).to.equal("0x" + "22".repeat(32)); // blockHash
expect(res[1]).to.equal(9241622n); // blockNumber
expect(res[3]).to.equal(VSROOT); // validatorSetRoot
expect(res[4]).to.equal(true); // finalized
// no state change
expect(await lc.finalizedNumber()).to.equal(BOOT_NUM);
expect(await lc.updateCount()).to.equal(0n);
});
it("verify() reverts on a wrong anchor even with a valid proof", async function () {
const bad = pv({ validatorSetRoot: "0x" + "ee".repeat(32) });
await accept(bad);
await expect(lc.verify(bad, PROOF)).to.be.revertedWithCustomError(lc, "ValidatorSetMismatch");
});
});