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.
425 lines
20 KiB
JavaScript
425 lines
20 KiB
JavaScript
// AereProofAggregatorV2 — regression tests for INNER VKEYS COUNTED BUT NEVER ENFORCED.
|
|
//
|
|
// FINDING (2026-07-17 sweep, code-read): AereProofAggregator 0x6a260238…F84C5 pins the
|
|
// aggregation vkey and the guest is genuinely recursive — but the contract only COUNTED how
|
|
// many folded inner vkeys were registered (into a `recognized` field) and recorded the
|
|
// aggregation regardless. An unregistered inner program was not an error, just a lower number.
|
|
//
|
|
// THE ATTACK: an attacker writes an SP1 guest with NO constraints —
|
|
// fn main() { sp1_zkvm::io::commit_slice(&attacker_chosen_bytes); }
|
|
// — has it commit the exact AereZKScreen public tuple, proves it (trivial: it asserts nothing),
|
|
// folds it with the REAL aggregation guest (which honestly verifies it, because it IS a valid
|
|
// SP1 proof of that vacuous program), and submits. V1 records an aggregation that `exists`,
|
|
// carries the pinned aggVKey, and is indistinguishable from a genuine one except for
|
|
// `recognized < count` — a field nothing ever enforced.
|
|
//
|
|
// Each attack test is PAIRED with a control on the REAL V1 contract showing the same call
|
|
// SUCCEEDS on V1, i.e. the bug was real and V2 closes it.
|
|
//
|
|
// THE GUEST IS UNCHANGED. The composite the guest commits ALREADY binds every inner vkey
|
|
// digest, and V1 already recomputed it correctly — so the contract always KNEW which programs
|
|
// were folded. The fix is purely what the contract DOES with that knowledge. This is verified
|
|
// below: the composite recomputed by these tests reproduces the REAL live on-chain aggregation
|
|
// digest 0x8e246396…4e36 byte-for-byte.
|
|
//
|
|
// The SP1 verifier is MOCKED (MockSp1Verifier: reverts unless the exact (vkey, publicValues,
|
|
// proof) triple was pre-registered — mirroring the real gateway). That is the CORRECT model:
|
|
// the attacker's aggregation proof is CRYPTOGRAPHICALLY VALID (the recursion honestly verified
|
|
// a real proof of a real — if vacuous — program), so the gateway accepts it. The bug is not a
|
|
// broken proof; it is a proof about an unvetted program.
|
|
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
const PROOF = "0x4388a21c1234"; // opaque; the mock keys acceptance on the triple
|
|
|
|
/* ---------------- REAL live values (chain 2800, read 2026-07-17) ------------------ */
|
|
// The pinned aggregation program (registered on the live V1).
|
|
const AGG_VKEY = "0x003a70854f258b13c6612b0272421f2d7d97f90eee63e124864c80be61a2672c";
|
|
// The three REAL registered inner programs: KoalaBear digest (what the recursion binds)
|
|
// + BN254 on-chain vkey (what is published) + label.
|
|
const REAL_INNERS = [
|
|
{
|
|
label: "zkscreen",
|
|
digest: "0x3510880a6bb8ae42705a4cd76f3aa69422a8d836122361ba12a913635de3e5b7",
|
|
onChain: "0x006a211015aee2b90b82d266bef3aa6944551b06c488d86e895489b1dde3e5b7",
|
|
pvHash: "0xe4d363f0108963cb592f1c592d855a46b6d9579cccef2d4689b93c2441c78d88",
|
|
},
|
|
{
|
|
label: "over18",
|
|
digest: "0x2d54a563446f19740eab67524f3622a67463b716375c230a30d2b0fe633343c5",
|
|
onChain: "0x005aa94ac711bc65d0755b3a94f3622a6e8c76e2cdd708c29869587f633343c5",
|
|
pvHash: "0x44d77dcf2f60c490ffad39fb2cbe75648ae3c402ce6cbe1ac29f6525af3c6ce6",
|
|
},
|
|
{
|
|
label: "zkml-mnist",
|
|
digest: "0x30a21e711cd157425f9b5e2203ea0a525cb86cbc625b720f3313ce3727c4bceb",
|
|
onChain: "0x0061443ce273455d0afcdaf1103ea0a52b970d979896dc83d989e71ba7c4bceb",
|
|
pvHash: "0xa04ed64252a5388bee0a757a0c2f83da7594cfaee6c7c25e6b873f46f43aad7e",
|
|
},
|
|
];
|
|
// The composite the LIVE contract actually recorded for those three (aggregationCount==2).
|
|
const REAL_COMPOSITE = "0x8e246396a2f40c837501d06c965163db644284ffc877d7cd0d38681b612b4e36";
|
|
|
|
const FOUNDATION = "0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3"; // live V1 owner
|
|
|
|
// The attacker's unconstrained guest: a real SP1 program, so it has a real vkey digest —
|
|
// just one that was never vetted or registered.
|
|
const ATTACKER_DIGEST = "0x" + "de".repeat(32);
|
|
|
|
// composite = sha256( u32_be(n) || (digest_i || pvHash_i)... ) — the guest's formula.
|
|
function composite(digests, pvHashes) {
|
|
let packed = ethers.solidityPacked(["uint32"], [digests.length]);
|
|
for (let i = 0; i < digests.length; i++) packed = ethers.concat([packed, digests[i], pvHashes[i]]);
|
|
return ethers.sha256(packed);
|
|
}
|
|
function marker(vkey, pv, proof) {
|
|
return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes32", "bytes", "bytes"], [vkey, pv, proof]));
|
|
}
|
|
|
|
describe("AereProofAggregatorV2 — inner-vkey enforcement fix (2026-07-17)", function () {
|
|
let mock, v1, v2, owner, attacker;
|
|
|
|
const digests = REAL_INNERS.map((p) => p.digest);
|
|
const pvHashes = REAL_INNERS.map((p) => p.pvHash);
|
|
|
|
async function accept(vkey, pv, proof = PROOF) {
|
|
await mock.setValid(marker(vkey, pv, proof), true);
|
|
}
|
|
|
|
async function deployV2(seal = false) {
|
|
return await (await ethers.getContractFactory("AereProofAggregatorV2")).deploy(
|
|
await mock.getAddress(),
|
|
seal ? ethers.ZeroAddress : owner.address,
|
|
[AGG_VKEY],
|
|
["recursive-aggregation agg-program (real, live)"],
|
|
digests,
|
|
REAL_INNERS.map((p) => p.onChain),
|
|
REAL_INNERS.map((p) => p.label),
|
|
seal
|
|
);
|
|
}
|
|
|
|
beforeEach(async function () {
|
|
[owner, attacker] = await ethers.getSigners();
|
|
mock = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
|
|
await mock.waitForDeployment();
|
|
|
|
// The REAL V1 contract, registered EXACTLY as the live one is — the control.
|
|
v1 = await (await ethers.getContractFactory("AereProofAggregator")).deploy(await mock.getAddress());
|
|
await v1.waitForDeployment();
|
|
await v1.registerAggProgram(AGG_VKEY, "recursive-aggregation agg-program (real, live)");
|
|
for (const p of REAL_INNERS) await v1.registerInnerProgram(p.digest, p.onChain, p.label);
|
|
|
|
v2 = await deployV2(false);
|
|
await v2.waitForDeployment();
|
|
});
|
|
|
|
/* ---------------- the guest is unchanged: composite reproduces live data --------- */
|
|
|
|
describe("the recursion guest is UNCHANGED (the composite already bound the inner vkeys)", function () {
|
|
it("recomputes the REAL live on-chain composite 0x8e246396…4e36 byte-for-byte", async function () {
|
|
expect(composite(digests, pvHashes)).to.equal(REAL_COMPOSITE);
|
|
});
|
|
|
|
it("V2's on-chain _composite matches the guest formula on the REAL live inner set", async function () {
|
|
await accept(AGG_VKEY, REAL_COMPOSITE);
|
|
// verifyAggregation returns the composite it recomputed on-chain
|
|
expect(await v2.verifyAggregation(AGG_VKEY, digests, pvHashes, REAL_COMPOSITE, PROOF)).to.equal(REAL_COMPOSITE);
|
|
});
|
|
|
|
it("V1 recomputed it correctly too — V1 always KNEW which programs were folded", async function () {
|
|
await accept(AGG_VKEY, REAL_COMPOSITE);
|
|
expect(await v1.verifyAggregation(AGG_VKEY, digests, pvHashes, REAL_COMPOSITE, PROOF)).to.equal(REAL_COMPOSITE);
|
|
});
|
|
});
|
|
|
|
/* ================================================================================
|
|
THE ATTACK: fold a proof of an UNCONSTRAINED, unregistered guest.
|
|
================================================================================ */
|
|
|
|
describe("ATTACK — unconstrained attacker guest folded into an aggregation", function () {
|
|
// The attacker's vacuous guest commits the EXACT zkscreen public tuple (so its pvHash is
|
|
// the real zkscreen one), but the program that produced it is the attacker's own.
|
|
const atkDigests = () => [ATTACKER_DIGEST];
|
|
const atkPvHashes = () => [REAL_INNERS[0].pvHash]; // the real AereZKScreen public-values hash
|
|
const atkComposite = () => composite(atkDigests(), atkPvHashes());
|
|
|
|
it("PAIRED CONTROL: V1 RECORDS the attacker's aggregation as if legitimate", async function () {
|
|
const c = atkComposite();
|
|
await accept(AGG_VKEY, c);
|
|
await expect(v1.connect(attacker).recordAggregation(AGG_VKEY, atkDigests(), atkPvHashes(), c, PROOF)).to.emit(
|
|
v1,
|
|
"AggregationVerified"
|
|
);
|
|
|
|
const rec = await v1.getAggregation(c);
|
|
expect(rec[0]).to.equal(true); // exists <-- a forged aggregation is ON THE BOOKS
|
|
expect(rec[1]).to.equal(AGG_VKEY); // carries the pinned, legitimate agg vkey
|
|
expect(rec[2]).to.equal(1); // count
|
|
expect(rec[3]).to.equal(0); // recognized == 0: the ONLY tell, and nothing enforced it
|
|
expect(await v1.aggregationCount()).to.equal(1n);
|
|
});
|
|
|
|
it("V2 REVERTS with UnregisteredInnerProgram — the attack is closed", async function () {
|
|
const c = atkComposite();
|
|
await accept(AGG_VKEY, c);
|
|
await expect(v2.connect(attacker).recordAggregation(AGG_VKEY, atkDigests(), atkPvHashes(), c, PROOF))
|
|
.to.be.revertedWithCustomError(v2, "UnregisteredInnerProgram")
|
|
.withArgs(ATTACKER_DIGEST, 0);
|
|
expect((await v2.getAggregation(c))[0]).to.equal(false); // nothing recorded
|
|
expect(await v2.aggregationCount()).to.equal(0n);
|
|
});
|
|
|
|
it("V2's verifyAggregation view REVERTS too (a view used as an oracle must bind)", async function () {
|
|
const c = atkComposite();
|
|
await accept(AGG_VKEY, c);
|
|
await expect(v2.verifyAggregation(AGG_VKEY, atkDigests(), atkPvHashes(), c, PROOF))
|
|
.to.be.revertedWithCustomError(v2, "UnregisteredInnerProgram")
|
|
.withArgs(ATTACKER_DIGEST, 0);
|
|
});
|
|
|
|
it("PAIRED CONTROL: V1's verifyAggregation view ACCEPTS the attacker's fold", async function () {
|
|
const c = atkComposite();
|
|
await accept(AGG_VKEY, c);
|
|
expect(await v1.verifyAggregation(AGG_VKEY, atkDigests(), atkPvHashes(), c, PROOF)).to.equal(c);
|
|
});
|
|
});
|
|
|
|
describe("ATTACK — attacker guest SMUGGLED IN alongside real registered programs", function () {
|
|
// Harder to spot: 3 real programs + 1 attacker program. V1 reports recognized=3, count=4.
|
|
const mixDigests = () => [...digests, ATTACKER_DIGEST];
|
|
const mixPvHashes = () => [...pvHashes, "0x" + "77".repeat(32)];
|
|
const mixComposite = () => composite(mixDigests(), mixPvHashes());
|
|
|
|
it("PAIRED CONTROL: V1 records it (recognized=3 of count=4) — looks almost legitimate", async function () {
|
|
const c = mixComposite();
|
|
await accept(AGG_VKEY, c);
|
|
await v1.connect(attacker).recordAggregation(AGG_VKEY, mixDigests(), mixPvHashes(), c, PROOF);
|
|
const rec = await v1.getAggregation(c);
|
|
expect(rec[0]).to.equal(true);
|
|
expect(rec[2]).to.equal(4); // count
|
|
expect(rec[3]).to.equal(3); // recognized — 3 real programs carry the forged 4th
|
|
});
|
|
|
|
it("V2 REVERTS at the attacker's index (3), rejecting the whole aggregation", async function () {
|
|
const c = mixComposite();
|
|
await accept(AGG_VKEY, c);
|
|
await expect(v2.connect(attacker).recordAggregation(AGG_VKEY, mixDigests(), mixPvHashes(), c, PROOF))
|
|
.to.be.revertedWithCustomError(v2, "UnregisteredInnerProgram")
|
|
.withArgs(ATTACKER_DIGEST, 3);
|
|
expect(await v2.aggregationCount()).to.equal(0n);
|
|
});
|
|
});
|
|
|
|
describe("ATTACK — zero inner vkey digest (default value of an unregistered slot)", function () {
|
|
it("V2 REVERTS: a zero digest can never be allowed, so the gate cannot be no-op'd", async function () {
|
|
const d = [ethers.ZeroHash];
|
|
const h = [REAL_INNERS[0].pvHash];
|
|
const c = composite(d, h);
|
|
await accept(AGG_VKEY, c);
|
|
await expect(v2.recordAggregation(AGG_VKEY, d, h, c, PROOF))
|
|
.to.be.revertedWithCustomError(v2, "UnregisteredInnerProgram")
|
|
.withArgs(ethers.ZeroHash, 0);
|
|
});
|
|
|
|
it("V2 refuses to REGISTER a zero digest (would turn the gate into a no-op)", async function () {
|
|
await expect(v2.registerInnerProgram(ethers.ZeroHash, "0x" + "11".repeat(32), "evil"))
|
|
.to.be.revertedWithCustomError(v2, "ZeroVKey");
|
|
});
|
|
|
|
it("PAIRED CONTROL: V1 HAPPILY registers a zero digest", async function () {
|
|
await expect(v1.registerInnerProgram(ethers.ZeroHash, "0x" + "11".repeat(32), "evil")).to.not.be.reverted;
|
|
expect((await v1.innerPrograms(ethers.ZeroHash))[0]).to.equal(true);
|
|
});
|
|
});
|
|
|
|
/* ---------------- V2 does not over-reject: the honest path still works ----------- */
|
|
|
|
describe("V2 does not over-reject — real aggregations still record", function () {
|
|
it("records the REAL 3-program aggregation, recognized == count (the V2 invariant)", async function () {
|
|
await accept(AGG_VKEY, REAL_COMPOSITE);
|
|
await expect(v2.recordAggregation(AGG_VKEY, digests, pvHashes, REAL_COMPOSITE, PROOF)).to.emit(
|
|
v2,
|
|
"AggregationVerified"
|
|
);
|
|
const rec = await v2.getAggregation(REAL_COMPOSITE);
|
|
expect(rec[0]).to.equal(true);
|
|
expect(rec[1]).to.equal(AGG_VKEY);
|
|
expect(rec[2]).to.equal(3); // count
|
|
expect(rec[3]).to.equal(3); // recognized == count, ALWAYS, by construction
|
|
expect(rec[5]).to.equal(owner.address);
|
|
expect(await v2.aggregationCount()).to.equal(1n);
|
|
});
|
|
|
|
it("seeds the allowed set at deploy — functional with NO post-deploy signature", async function () {
|
|
for (const p of REAL_INNERS) {
|
|
const r = await v2.innerPrograms(p.digest);
|
|
expect(r[0]).to.equal(true);
|
|
expect(r[1]).to.equal(p.onChain);
|
|
expect(r[2]).to.equal(p.label);
|
|
}
|
|
expect(await v2.isAggProgram(AGG_VKEY)).to.equal(true);
|
|
expect(await v2.owner()).to.equal(owner.address);
|
|
expect(await v2.SP1_VERIFIER()).to.equal(await mock.getAddress());
|
|
});
|
|
|
|
it("areInnerProgramsAllowed() previews the gate without spending gas", async function () {
|
|
expect(await v2.areInnerProgramsAllowed(digests)).to.equal(true);
|
|
expect(await v2.areInnerProgramsAllowed([...digests, ATTACKER_DIGEST])).to.equal(false);
|
|
});
|
|
|
|
it("a newly-registered program becomes foldable (the governed extension path)", async function () {
|
|
const NEWD = "0x" + "5a".repeat(32);
|
|
const d = [NEWD];
|
|
const h = ["0x" + "99".repeat(32)];
|
|
const c = composite(d, h);
|
|
await accept(AGG_VKEY, c);
|
|
await expect(v2.recordAggregation(AGG_VKEY, d, h, c, PROOF)).to.be.revertedWithCustomError(
|
|
v2,
|
|
"UnregisteredInnerProgram"
|
|
);
|
|
await v2.registerInnerProgram(NEWD, "0x" + "6b".repeat(32), "new-program");
|
|
await expect(v2.recordAggregation(AGG_VKEY, d, h, c, PROOF)).to.emit(v2, "AggregationVerified");
|
|
});
|
|
});
|
|
|
|
/* ---------------- inherited protections still hold ------------------------------ */
|
|
|
|
describe("inherited protections (unchanged from V1)", function () {
|
|
it("an unregistered AGGREGATION program still reverts", async function () {
|
|
const bad = "0x" + "01".repeat(32);
|
|
await expect(v2.recordAggregation(bad, digests, pvHashes, REAL_COMPOSITE, PROOF))
|
|
.to.be.revertedWithCustomError(v2, "UnknownAggProgram")
|
|
.withArgs(bad);
|
|
});
|
|
|
|
it("a composite that does not match the committed public values reverts", async function () {
|
|
const wrong = "0x" + "ee".repeat(32);
|
|
await accept(AGG_VKEY, wrong);
|
|
await expect(v2.recordAggregation(AGG_VKEY, digests, pvHashes, wrong, PROOF)).to.be.revertedWithCustomError(
|
|
v2,
|
|
"CompositeMismatch"
|
|
);
|
|
});
|
|
|
|
it("an invalid Groth16 proof reverts InvalidProof", async function () {
|
|
await expect(v2.recordAggregation(AGG_VKEY, digests, pvHashes, REAL_COMPOSITE, PROOF))
|
|
.to.be.revertedWithCustomError(v2, "InvalidProof");
|
|
});
|
|
|
|
it("dropping a folded child changes the composite => rejected", async function () {
|
|
await accept(AGG_VKEY, REAL_COMPOSITE);
|
|
await expect(
|
|
v2.recordAggregation(AGG_VKEY, digests.slice(0, 2), pvHashes.slice(0, 2), REAL_COMPOSITE, PROOF)
|
|
).to.be.revertedWithCustomError(v2, "CompositeMismatch");
|
|
});
|
|
|
|
it("empty / mismatched-length / wrong-pv-length all revert", async function () {
|
|
await expect(v2.recordAggregation(AGG_VKEY, [], [], REAL_COMPOSITE, PROOF)).to.be.revertedWithCustomError(
|
|
v2,
|
|
"EmptyAggregation"
|
|
);
|
|
await expect(
|
|
v2.recordAggregation(AGG_VKEY, digests, pvHashes.slice(0, 2), REAL_COMPOSITE, PROOF)
|
|
).to.be.revertedWithCustomError(v2, "LengthMismatch");
|
|
await expect(v2.recordAggregation(AGG_VKEY, digests, pvHashes, "0x1234", PROOF))
|
|
.to.be.revertedWithCustomError(v2, "BadPublicValuesLength")
|
|
.withArgs(2);
|
|
});
|
|
});
|
|
|
|
/* ---------------- registry governance + the seal latch -------------------------- */
|
|
|
|
describe("registry governance and the one-way seal", function () {
|
|
it("only the owner may register", async function () {
|
|
await expect(
|
|
v2.connect(attacker).registerInnerProgram("0x" + "5a".repeat(32), "0x" + "6b".repeat(32), "evil")
|
|
).to.be.revertedWith("Ownable: caller is not the owner");
|
|
await expect(v2.connect(attacker).registerAggProgram("0x" + "5a".repeat(32), "evil")).to.be.revertedWith(
|
|
"Ownable: caller is not the owner"
|
|
);
|
|
});
|
|
|
|
it("sealRegistry() permanently freezes the set and renounces ownership", async function () {
|
|
await expect(v2.sealRegistry()).to.emit(v2, "RegistrySealed");
|
|
expect(await v2.registrySealed()).to.equal(true);
|
|
expect(await v2.owner()).to.equal(ethers.ZeroAddress);
|
|
// no one can extend the set any more — not even the old owner
|
|
await expect(
|
|
v2.registerInnerProgram("0x" + "5a".repeat(32), "0x" + "6b".repeat(32), "post-seal")
|
|
).to.be.revertedWith("Ownable: caller is not the owner");
|
|
await expect(v2.sealRegistry()).to.be.revertedWith("Ownable: caller is not the owner");
|
|
});
|
|
|
|
it("sealed aggregations still record (sealing freezes the SET, not the contract)", async function () {
|
|
await v2.sealRegistry();
|
|
await accept(AGG_VKEY, REAL_COMPOSITE);
|
|
await expect(v2.recordAggregation(AGG_VKEY, digests, pvHashes, REAL_COMPOSITE, PROOF)).to.emit(
|
|
v2,
|
|
"AggregationVerified"
|
|
);
|
|
});
|
|
|
|
it("can be deployed SEALED at construction: seeded set, zero admin, ever", async function () {
|
|
const sealed = await deployV2(true);
|
|
await sealed.waitForDeployment();
|
|
expect(await sealed.registrySealed()).to.equal(true);
|
|
expect(await sealed.owner()).to.equal(ethers.ZeroAddress);
|
|
expect(await sealed.isAggProgram(AGG_VKEY)).to.equal(true);
|
|
expect((await sealed.innerPrograms(REAL_INNERS[0].digest))[0]).to.equal(true);
|
|
// and it still rejects the attack
|
|
const c = composite([ATTACKER_DIGEST], [REAL_INNERS[0].pvHash]);
|
|
await accept(AGG_VKEY, c);
|
|
await expect(
|
|
sealed.recordAggregation(AGG_VKEY, [ATTACKER_DIGEST], [REAL_INNERS[0].pvHash], c, PROOF)
|
|
).to.be.revertedWithCustomError(sealed, "UnregisteredInnerProgram");
|
|
});
|
|
});
|
|
|
|
/* ---------------- constructor guards -------------------------------------------- */
|
|
|
|
describe("constructor guards", function () {
|
|
let F;
|
|
beforeEach(async function () {
|
|
F = await ethers.getContractFactory("AereProofAggregatorV2");
|
|
});
|
|
|
|
it("rejects a code-less SP1 verifier (V1 accepted one)", async function () {
|
|
await expect(
|
|
F.deploy("0x000000000000000000000000000000000000dEaD", owner.address, [], [], [], [], [], false)
|
|
).to.be.revertedWithCustomError(F, "VerifierHasNoCode");
|
|
});
|
|
|
|
it("rejects the zero verifier / zero owner when not sealed", async function () {
|
|
await expect(F.deploy(ethers.ZeroAddress, owner.address, [], [], [], [], [], false)).to.be.revertedWithCustomError(
|
|
F,
|
|
"ZeroAddress"
|
|
);
|
|
await expect(
|
|
F.deploy(await mock.getAddress(), ethers.ZeroAddress, [], [], [], [], [], false)
|
|
).to.be.revertedWithCustomError(F, "ZeroAddress");
|
|
});
|
|
|
|
it("rejects mismatched seed array lengths", async function () {
|
|
const m = await mock.getAddress();
|
|
await expect(F.deploy(m, owner.address, [AGG_VKEY], [], [], [], [], false)).to.be.revertedWithCustomError(
|
|
F,
|
|
"SeedLengthMismatch"
|
|
);
|
|
await expect(
|
|
F.deploy(m, owner.address, [], [], digests, [], [], false)
|
|
).to.be.revertedWithCustomError(F, "SeedLengthMismatch");
|
|
});
|
|
|
|
it("rejects seeding a zero inner digest or zero agg vkey", async function () {
|
|
const m = await mock.getAddress();
|
|
await expect(
|
|
F.deploy(m, owner.address, [], [], [ethers.ZeroHash], ["0x" + "11".repeat(32)], ["evil"], false)
|
|
).to.be.revertedWithCustomError(F, "ZeroVKey");
|
|
await expect(
|
|
F.deploy(m, owner.address, [ethers.ZeroHash], ["evil"], [], [], [], false)
|
|
).to.be.revertedWithCustomError(F, "ZeroVKey");
|
|
});
|
|
});
|
|
});
|