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