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