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

366 lines
17 KiB
JavaScript

const { expect } = require("chai");
const { ethers } = require("hardhat");
// ---------------------------------------------------------------------------
// AerePQFinalityCertificate: the ADDITIVE Post-Quantum Finality Certificate.
//
// WHAT IS UNDER TEST (real, on-chain): AerePQAttestationKeyRegistry (per-validator
// hash-based attestation keys + a deterministic validator-set root) and
// AereFinalityCertificateVerifier (binds ONE succinct SP1 proof to the registry root,
// enforces the ceil(2N/3) quorum, records PQ finality, exposes isFinalPQ).
//
// TEST DOUBLE: MockSp1Verifier is the deployed SP1 gateway stand-in. Its verifyProof
// REVERTS on an invalid proof (exact production gateway semantics), so it faithfully
// exercises the fail-closed path. It accepts only (vkey, publicValues, proof) triples
// pre-registered via setValid(), so a "valid proof" here means the gateway asserted the
// exact bound public values.
//
// HONEST BOUNDARY: the OFF-CHAIN SP1 aggregation guest circuit (verify N hash-based
// XMSS/leanSig signatures against the validator-set root inside the zkVM and emit these
// public values) is NOT implemented and is [MEASURE]. These tests prove the on-chain
// verifier + registry only, against a mock gateway, exactly like AerePQAggregate and the
// zk light clients. Aere consensus stays classical ECDSA QBFT; this is purely additive.
// ---------------------------------------------------------------------------
const SCHEME_XMSS = 1;
const SCHEME_LEANSIG = 3;
function abicoder() {
return ethers.AbiCoder.defaultAbiCoder();
}
// The fixed 192-byte SP1 public-values layout the aggregation circuit must emit.
function encodePV({ blockHash, height, root, size, quorum, domain }) {
return abicoder().encode(
["bytes32", "uint256", "bytes32", "uint256", "uint256", "bytes32"],
[blockHash, height, root, size, quorum, domain]
);
}
// Marker the MockSp1Verifier keys a valid proof on: keccak256(abi.encode(vkey, publicValues, proof)).
function proofMarker(vkey, publicValues, proof) {
return ethers.keccak256(abicoder().encode(["bytes32", "bytes", "bytes"], [vkey, publicValues, proof]));
}
// A 48-byte hash-based (XMSS-style) attestation public key: root(32) || seed(16).
function xmssKey(label) {
const root = ethers.keccak256(ethers.toUtf8Bytes("xmss-root:" + label));
const seed = ethers.keccak256(ethers.toUtf8Bytes("xmss-seed:" + label));
return ethers.concat([root, ethers.dataSlice(seed, 0, 16)]);
}
describe("AerePQFinalityCertificate (attestation-key registry + certificate verifier)", function () {
let sp1, registry, verifier;
let deployer, relayer, other;
let validators; // 7 signers
const VKEY = ethers.keccak256(ethers.toUtf8Bytes("sp1-program:aere-pq-finality-cert-v1"));
const BLOCK_HASH = ethers.keccak256(ethers.toUtf8Bytes("aere-block:12345678"));
const BLOCK_HEIGHT = 12345678n;
beforeEach(async function () {
const signers = await ethers.getSigners();
deployer = signers[0];
relayer = signers[8];
other = signers[9];
validators = signers.slice(1, 8); // 7 validators
sp1 = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
await sp1.waitForDeployment();
registry = await (await ethers.getContractFactory("AerePQAttestationKeyRegistry")).deploy();
await registry.waitForDeployment();
verifier = await (await ethers.getContractFactory("AereFinalityCertificateVerifier")).deploy(
await sp1.getAddress(),
VKEY,
await registry.getAddress()
);
await verifier.waitForDeployment();
// Enroll the 7-validator set (owner), then each validator self-registers its PQ key.
for (const v of validators) {
await registry.connect(deployer).enrollValidator(v.address);
}
for (const v of validators) {
await registry.connect(v).registerAttestationKey(SCHEME_XMSS, xmssKey(v.address));
}
});
// Build public values bound to the CURRENT registry state (unless overridden), and
// (optionally) register the proof valid in the mock gateway.
async function makeCert({
blockHash = BLOCK_HASH,
height = BLOCK_HEIGHT,
quorum,
proof = "0xabcd01",
overrideRoot = null,
overrideSize = null,
overrideDomain = null,
register = true,
}) {
const root = overrideRoot !== null ? overrideRoot : await registry.validatorSetRoot();
const size = overrideSize !== null ? overrideSize : await registry.validatorCount();
const domain = overrideDomain !== null ? overrideDomain : await verifier.attestationDomain();
const pv = encodePV({ blockHash, height, root, size, quorum, domain });
if (register) {
await sp1.setValid(proofMarker(VKEY, pv, proof), true);
}
return { pv, proof };
}
// =========================================================================
// 1. Registry: 7-validator set with hash-based attestation keys
// =========================================================================
describe("attestation-key registry", function () {
it("registers a 7-validator set with hash-based keys, quorum threshold is 5", async function () {
expect(await registry.validatorCount()).to.equal(7n);
expect(await registry.isSetComplete()).to.equal(true);
expect(await registry.activeKeyCount()).to.equal(7n);
expect(await verifier.currentQuorumThreshold()).to.equal(5n); // ceil(2*7/3) = 5
for (const v of validators) {
expect(await registry.isEnrolled(v.address)).to.equal(true);
expect(await registry.keyEpochOf(v.address)).to.equal(1n);
const [keyId, scheme, epoch] = await registry.currentKey(v.address);
expect(scheme).to.equal(SCHEME_XMSS);
expect(epoch).to.equal(1n);
expect(keyId).to.not.equal(ethers.MaxUint256);
}
expect(await registry.validatorSetRoot()).to.not.equal(ethers.ZeroHash);
});
it("is append-only and fail-closed: only enrolled validators register; enroll is idempotent-guarded", async function () {
// A non-enrolled address cannot register a key.
await expect(
registry.connect(other).registerAttestationKey(SCHEME_XMSS, xmssKey("intruder"))
).to.be.revertedWithCustomError(registry, "NotEnrolled");
// Re-enrolling an existing validator reverts (no silent re-ordering).
await expect(
registry.connect(deployer).enrollValidator(validators[0].address)
).to.be.revertedWithCustomError(registry, "AlreadyEnrolled");
// Only the owner may enroll.
await expect(
registry.connect(other).enrollValidator(other.address)
).to.be.revertedWith("Ownable: caller is not the owner");
// Bad scheme / bad key length are rejected.
await expect(
registry.connect(validators[0]).registerAttestationKey(99, xmssKey("x"))
).to.be.revertedWithCustomError(registry, "InvalidScheme");
await expect(
registry.connect(validators[0]).registerAttestationKey(SCHEME_XMSS, "0x1234")
).to.be.revertedWithCustomError(registry, "InvalidPubKey");
});
});
// =========================================================================
// 2. A valid certificate with quorum 5 records and isFinalPQ returns true
// =========================================================================
describe("valid certificate records PQ finality", function () {
it("quorum 5 verifies, records, and isFinalPQ(blockHash) is true", async function () {
const { pv, proof } = await makeCert({ quorum: 5 });
// Strict view path returns the finalized block.
const [bh, height] = await verifier.verifyCertificate(pv, proof);
expect(bh).to.equal(BLOCK_HASH);
expect(height).to.equal(BLOCK_HEIGHT);
// Non-reverting path is true.
expect(await verifier.tryVerifyCertificate(pv, proof)).to.equal(true);
// Consumer confirm-a-specific-block path.
expect(await verifier.assertFinalizes(BLOCK_HASH, BLOCK_HEIGHT, pv, proof)).to.equal(true);
// isFinalPQ is false BEFORE recording.
expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(false);
// Recording emits and stores provenance.
await expect(verifier.connect(relayer).recordCertificate(pv, proof))
.to.emit(verifier, "CertificateRecorded")
.withArgs(BLOCK_HASH, BLOCK_HEIGHT, 5, 7, await registry.validatorSetRoot(), relayer.address);
expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(true);
expect(await verifier.isFinalPQAtHeight(BLOCK_HASH, BLOCK_HEIGHT)).to.equal(true);
expect(await verifier.highestFinalizedHeight()).to.equal(BLOCK_HEIGHT);
expect(await verifier.highestFinalizedBlockHash()).to.equal(BLOCK_HASH);
expect(await verifier.certificateCount()).to.equal(1n);
const cert = await verifier.certificateOf(BLOCK_HASH);
expect(cert.exists).to.equal(true);
expect(cert.quorumCount).to.equal(5);
expect(cert.validatorSetSize).to.equal(7);
expect(cert.prover).to.equal(relayer.address);
});
it("full quorum (7) also records", async function () {
const { pv, proof } = await makeCert({ quorum: 7, proof: "0xf011" });
await verifier.recordCertificate(pv, proof);
expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(true);
});
});
// =========================================================================
// 3. A certificate below quorum rejects
// =========================================================================
describe("below-quorum rejects fail-closed", function () {
it("quorum 4 (< 5) is rejected even with a gateway-valid proof", async function () {
const { pv, proof } = await makeCert({ quorum: 4 });
await expect(verifier.verifyCertificate(pv, proof)).to.be.revertedWithCustomError(verifier, "BelowQuorum");
expect(await verifier.tryVerifyCertificate(pv, proof)).to.equal(false);
await expect(verifier.recordCertificate(pv, proof)).to.be.revertedWithCustomError(verifier, "BelowQuorum");
expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(false);
});
it("an invalid proof (not registered in the gateway) is rejected fail-closed", async function () {
const { pv, proof } = await makeCert({ quorum: 5, register: false });
await expect(verifier.verifyCertificate(pv, proof)).to.be.revertedWithCustomError(sp1, "MockInvalidProof");
expect(await verifier.tryVerifyCertificate(pv, proof)).to.equal(false);
expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(false);
});
});
// =========================================================================
// 4. A wrong validator-set root rejects
// =========================================================================
describe("wrong validator-set root rejects", function () {
it("a proof carrying a forged root is rejected before the gateway call", async function () {
const forgedRoot = ethers.keccak256(ethers.toUtf8Bytes("attacker-validator-set"));
const { pv, proof } = await makeCert({ quorum: 5, overrideRoot: forgedRoot });
await expect(verifier.verifyCertificate(pv, proof)).to.be.revertedWithCustomError(
verifier,
"ValidatorSetRootMismatch"
);
expect(await verifier.tryVerifyCertificate(pv, proof)).to.equal(false);
});
it("a proof carrying the wrong set size or domain is rejected", async function () {
const wrongSize = await makeCert({ quorum: 5, overrideSize: 9, proof: "0x51" });
await expect(verifier.verifyCertificate(wrongSize.pv, wrongSize.proof)).to.be.revertedWithCustomError(
verifier,
"SizeMismatch"
);
const wrongDomain = await makeCert({
quorum: 5,
overrideDomain: ethers.keccak256(ethers.toUtf8Bytes("wrong-domain")),
proof: "0x52",
});
await expect(verifier.verifyCertificate(wrongDomain.pv, wrongDomain.proof)).to.be.revertedWithCustomError(
verifier,
"DomainMismatch"
);
// Malformed (not 192 bytes) public values reject.
await expect(verifier.verifyCertificate("0x1234", "0x53")).to.be.revertedWithCustomError(
verifier,
"BadPublicValues"
);
});
});
// =========================================================================
// 5. A wrong block rejects
// =========================================================================
describe("wrong block rejects", function () {
it("assertFinalizes rejects a certificate substituted for a different block", async function () {
const { pv, proof } = await makeCert({ quorum: 5 });
const otherBlock = ethers.keccak256(ethers.toUtf8Bytes("aere-block:99999999"));
// The proof genuinely finalizes BLOCK_HASH...
expect(await verifier.assertFinalizes(BLOCK_HASH, BLOCK_HEIGHT, pv, proof)).to.equal(true);
// ...but cannot be claimed for a different block.
await expect(
verifier.assertFinalizes(otherBlock, BLOCK_HEIGHT, pv, proof)
).to.be.revertedWithCustomError(verifier, "BlockHashMismatch");
// ...nor a different height.
await expect(
verifier.assertFinalizes(BLOCK_HASH, BLOCK_HEIGHT + 1n, pv, proof)
).to.be.revertedWithCustomError(verifier, "BlockHeightMismatch");
expect(await verifier.tryAssertFinalizes(otherBlock, BLOCK_HEIGHT, pv, proof)).to.equal(false);
});
it("a zero block hash is rejected", async function () {
const { pv, proof } = await makeCert({ blockHash: ethers.ZeroHash, quorum: 5, proof: "0x2000" });
await expect(verifier.verifyCertificate(pv, proof)).to.be.revertedWithCustomError(verifier, "ZeroBlockHash");
});
});
// =========================================================================
// 6. Key rotation updates the set root; an old-root certificate no longer matches
// =========================================================================
describe("key rotation invalidates old-root certificates", function () {
it("rotating one validator's key changes the root and rejects the pre-rotation certificate", async function () {
const oldRoot = await registry.validatorSetRoot();
// A certificate valid against the CURRENT (pre-rotation) root.
const { pv: oldPV, proof: oldProof } = await makeCert({ quorum: 5, proof: "0x01d0" });
// It verifies now.
expect(await verifier.tryVerifyCertificate(oldPV, oldProof)).to.equal(true);
// Validator 0 rotates its one-time/few-time key (epoch 1 -> 2), scheme change allowed.
await registry.connect(validators[0]).registerAttestationKey(SCHEME_LEANSIG, xmssKey("rotated-key-0"));
expect(await registry.keyEpochOf(validators[0].address)).to.equal(2n);
const newRoot = await registry.validatorSetRoot();
expect(newRoot).to.not.equal(oldRoot);
// The pre-rotation certificate (bound to oldRoot) no longer matches the live registry.
await expect(verifier.verifyCertificate(oldPV, oldProof)).to.be.revertedWithCustomError(
verifier,
"ValidatorSetRootMismatch"
);
expect(await verifier.tryVerifyCertificate(oldPV, oldProof)).to.equal(false);
// A fresh certificate bound to the NEW root verifies and records.
const { pv: newPV, proof: newProof } = await makeCert({ quorum: 5, proof: "0x0ec0" });
await verifier.recordCertificate(newPV, newProof);
expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(true);
// History is append-only: validator 0 has two keys on record.
const history = await registry.keyHistoryOf(validators[0].address);
expect(history.length).to.equal(2);
const first = await registry.getKey(history[0]);
expect(first.superseded).to.equal(true);
const second = await registry.getKey(history[1]);
expect(second.superseded).to.equal(false);
expect(second.scheme).to.equal(SCHEME_LEANSIG);
});
});
// =========================================================================
// 7. Empty validator set fails closed
// =========================================================================
describe("empty validator set fails closed", function () {
it("a verifier over an empty registry rejects any certificate", async function () {
const emptyRegistry = await (await ethers.getContractFactory("AerePQAttestationKeyRegistry")).deploy();
await emptyRegistry.waitForDeployment();
const emptyVerifier = await (await ethers.getContractFactory("AereFinalityCertificateVerifier")).deploy(
await sp1.getAddress(),
VKEY,
await emptyRegistry.getAddress()
);
await emptyVerifier.waitForDeployment();
const domain = await emptyVerifier.attestationDomain();
const pv = encodePV({
blockHash: BLOCK_HASH,
height: BLOCK_HEIGHT,
root: await emptyRegistry.validatorSetRoot(),
size: 0,
quorum: 0,
domain,
});
await sp1.setValid(proofMarker(VKEY, pv, "0xee"), true);
await expect(emptyVerifier.verifyCertificate(pv, "0xee")).to.be.revertedWithCustomError(
emptyVerifier,
"EmptyValidatorSet"
);
});
});
});