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