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

224 lines
11 KiB
JavaScript

// AereZKScreen + AereAIProof tests.
const { expect } = require("chai");
const { ethers } = require("hardhat");
async function deployZkStack() {
const [deployer, alice, bob] = await ethers.getSigners();
const Verifier = await ethers.getContractFactory("MockSp1Verifier");
const verifier = await Verifier.deploy();
await verifier.waitForDeployment();
const ZK = await ethers.getContractFactory("AereZKScreen");
const zk = await ZK.deploy(await verifier.getAddress());
await zk.waitForDeployment();
return { deployer, alice, bob, verifier, zk };
}
// Build publicValues + pre-program the void mock to accept the exact triple.
async function makeProof(verifier, vKey, user, attestedAt, extraDataHash, proof = "0xdeadbeef") {
const pv = ethers.AbiCoder.defaultAbiCoder().encode(
["uint256", "address", "uint256", "bytes32"],
[(await ethers.provider.getNetwork()).chainId, user, attestedAt, extraDataHash]
);
const marker = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
["bytes32", "bytes", "bytes"], [vKey, pv, proof]
));
await verifier.setValid(marker, true);
return { pv, proof };
}
describe("AereZKScreen", function () {
const ROOT = ethers.id("authorized-allowlist-root-v1");
it("Foundation registers program; duplicate rejected; zero root rejected", async function () {
const { zk } = await deployZkStack();
const vKey = ethers.id("OFAC-screen-v1");
await zk.registerProgram(vKey, 90 * 86400, ROOT, "OFAC SDN screen v1");
await expect(zk.registerProgram(vKey, 0, ROOT, "")).to.be.revertedWithCustomError(zk, "AlreadyRegistered");
const vKey2 = ethers.id("zero-root");
await expect(zk.registerProgram(vKey2, 0, ethers.ZeroHash, "")).to.be.revertedWithCustomError(zk, "ZeroRoot");
});
it("submits valid proof (root == authorizedRoot); isCleared true", async function () {
const { alice, verifier, zk } = await deployZkStack();
const vKey = ethers.id("OFAC-screen-v1");
await zk.registerProgram(vKey, 90 * 86400, ROOT, "OFAC");
const attestedAt = (await ethers.provider.getBlock("latest")).timestamp - 60;
const { pv, proof } = await makeProof(verifier, vKey, alice.address, attestedAt, ROOT);
await zk.connect(alice).submitProof(vKey, pv, proof);
expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(true);
});
it("rejects proof whose root != authorizedRoot (self-clear attack)", async function () {
const { alice, verifier, zk } = await deployZkStack();
const vKey = ethers.id("OFAC-screen-v1");
await zk.registerProgram(vKey, 0, ROOT, "OFAC");
// Attacker proves membership in a root of their OWN making.
const attackerRoot = ethers.id("attacker-picked-root");
const attestedAt = (await ethers.provider.getBlock("latest")).timestamp - 60;
const { pv, proof } = await makeProof(verifier, vKey, alice.address, attestedAt, attackerRoot);
await expect(zk.connect(alice).submitProof(vKey, pv, proof))
.to.be.revertedWithCustomError(zk, "UnauthorizedRoot");
expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(false);
});
it("rejects proof for wrong user", async function () {
const { alice, bob, verifier, zk } = await deployZkStack();
const vKey = ethers.id("OFAC-screen-v1");
await zk.registerProgram(vKey, 0, ROOT, "OFAC");
const attestedAt = (await ethers.provider.getBlock("latest")).timestamp - 60;
const { pv, proof } = await makeProof(verifier, vKey, bob.address, attestedAt, ROOT);
await expect(zk.connect(alice).submitProof(vKey, pv, proof))
.to.be.revertedWithCustomError(zk, "UserMismatch");
});
it("rejects invalid proof", async function () {
const { alice, zk } = await deployZkStack();
const vKey = ethers.id("v");
await zk.registerProgram(vKey, 0, ROOT, "x");
const pv = ethers.AbiCoder.defaultAbiCoder().encode(
["uint256", "address", "uint256", "bytes32"],
[(await ethers.provider.getNetwork()).chainId, alice.address, 100, ROOT]
);
await expect(zk.connect(alice).submitProof(vKey, pv, "0x"))
.to.be.revertedWithCustomError(zk, "InvalidProof");
});
it("expiry causes isCleared to flip false", async function () {
const { alice, verifier, zk } = await deployZkStack();
const vKey = ethers.id("expiry-test");
await zk.registerProgram(vKey, 100, ROOT, "short");
const ts = (await ethers.provider.getBlock("latest")).timestamp - 50;
const { pv, proof } = await makeProof(verifier, vKey, alice.address, ts, ROOT, "0x01");
await zk.connect(alice).submitProof(vKey, pv, proof);
expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(true);
await ethers.provider.send("evm_increaseTime", [200]);
await ethers.provider.send("evm_mine", []);
expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(false);
});
it("setAuthorizedRoot rotates the accepted root", async function () {
const { alice, verifier, zk } = await deployZkStack();
const vKey = ethers.id("rotate");
await zk.registerProgram(vKey, 0, ROOT, "rot");
const newRoot = ethers.id("rotated-root");
// Proof against the OLD root now fails after rotation.
await zk.setAuthorizedRoot(vKey, newRoot);
const attestedAt = (await ethers.provider.getBlock("latest")).timestamp - 30;
const old = await makeProof(verifier, vKey, alice.address, attestedAt, ROOT);
await expect(zk.connect(alice).submitProof(vKey, old.pv, old.proof))
.to.be.revertedWithCustomError(zk, "UnauthorizedRoot");
// Proof against the NEW root succeeds.
const fresh = await makeProof(verifier, vKey, alice.address, attestedAt, newRoot);
await zk.connect(alice).submitProof(vKey, fresh.pv, fresh.proof);
expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(true);
await expect(zk.setAuthorizedRoot(vKey, ethers.ZeroHash)).to.be.revertedWithCustomError(zk, "ZeroRoot");
});
it("revoke voids a clearance; a fresh newer proof re-clears", async function () {
const { alice, verifier, zk } = await deployZkStack();
const vKey = ethers.id("revoke-test");
await zk.registerProgram(vKey, 0, ROOT, "rev");
const t1 = (await ethers.provider.getBlock("latest")).timestamp - 100;
const p1 = await makeProof(verifier, vKey, alice.address, t1, ROOT);
await zk.connect(alice).submitProof(vKey, p1.pv, p1.proof);
expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(true);
await zk.revoke(alice.address, vKey);
expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(false);
// A fresh proof with attestedAt strictly after the revocation re-clears.
await ethers.provider.send("evm_increaseTime", [10]);
await ethers.provider.send("evm_mine", []);
const t2 = (await ethers.provider.getBlock("latest")).timestamp;
const p2 = await makeProof(verifier, vKey, alice.address, t2, ROOT);
await zk.connect(alice).submitProof(vKey, p2.pv, p2.proof);
expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(true);
});
it("only owner can register, rotate, revoke", async function () {
const { alice, zk } = await deployZkStack();
const vKey = ethers.id("owner-test");
await expect(zk.connect(alice).registerProgram(vKey, 0, ROOT, "x"))
.to.be.revertedWith("Ownable: caller is not the owner");
await zk.registerProgram(vKey, 0, ROOT, "x");
await expect(zk.connect(alice).setAuthorizedRoot(vKey, ethers.id("y")))
.to.be.revertedWith("Ownable: caller is not the owner");
await expect(zk.connect(alice).revoke(alice.address, vKey))
.to.be.revertedWith("Ownable: caller is not the owner");
});
});
describe("AereAIProof", function () {
async function deploy() {
const [deployer, signer, operator, requester] = await ethers.getSigners();
const AI = await ethers.getContractFactory("AereAIProof");
const ai = await AI.deploy();
await ai.waitForDeployment();
return { deployer, signer, operator, requester, ai };
}
async function signAttestation(wallet, ai, attest) {
const domain = {
name: "AereAIProof", version: "1",
chainId: (await ethers.provider.getNetwork()).chainId,
verifyingContract: await ai.getAddress(),
};
const types = {
Attestation: [
{ name: "modelId", type: "bytes32" },
{ name: "inputHash", type: "bytes32" },
{ name: "outputHash", type: "bytes32" },
{ name: "requesterHash", type: "bytes32" },
{ name: "timestamp", type: "uint256" },
{ name: "nonce", type: "uint256" },
],
};
return wallet.signTypedData(domain, types, attest);
}
it("operator registers model; signer rotation works", async function () {
const { operator, signer, requester, ai } = await deploy();
const modelId = ethers.id("claude-opus-4.7");
await ai.connect(operator).registerModel(modelId, signer.address, "Claude Opus 4.7");
const m = await ai.models(modelId);
expect(m.signer).to.equal(signer.address);
expect(m.operator).to.equal(operator.address);
await ai.connect(operator).setSigner(modelId, requester.address);
expect((await ai.models(modelId)).signer).to.equal(requester.address);
});
it("anchors a valid attestation; rejects replay + bad-sig + wrong-chain timestamp", async function () {
const { operator, signer, requester, ai } = await deploy();
const modelId = ethers.id("claude-opus-4.7");
await ai.connect(operator).registerModel(modelId, signer.address, "Claude Opus 4.7");
const inputHash = ethers.id("input-X");
const outputHash = ethers.id("output-Y");
const requesterHash= ethers.id("user-Z-nonce-1");
const ts = (await ethers.provider.getBlock("latest")).timestamp - 10;
const nonce = 1n;
const attest = { modelId, inputHash, outputHash, requesterHash, timestamp: ts, nonce };
const sig = await signAttestation(signer, ai, attest);
await expect(ai.connect(requester).anchor(modelId, inputHash, outputHash, requesterHash, ts, nonce, sig))
.to.emit(ai, "Anchored");
expect(await ai.attestationCount(modelId)).to.equal(1n);
// Replay rejected.
await expect(ai.connect(requester).anchor(modelId, inputHash, outputHash, requesterHash, ts, nonce, sig))
.to.be.revertedWithCustomError(ai, "NonceConsumed");
// Wrong signer rejected.
const badSig = await signAttestation(operator, ai, { ...attest, nonce: 2n });
await expect(ai.connect(requester).anchor(modelId, inputHash, outputHash, requesterHash, ts, 2n, badSig))
.to.be.revertedWithCustomError(ai, "BadSignature");
// Future timestamp rejected.
const future = (await ethers.provider.getBlock("latest")).timestamp + 3600;
const futureSig = await signAttestation(signer, ai, { ...attest, nonce: 3n, timestamp: future });
await expect(ai.connect(requester).anchor(modelId, inputHash, outputHash, requesterHash, future, 3n, futureSig))
.to.be.revertedWithCustomError(ai, "AttestationInFuture");
});
});