// AereDACommittee — unit tests (pure ECDSA M-of-N, runs fully local). const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("AereDACommittee", function () { let dac, admin, m1, m2, m3, outsider; let members; // sorted ascending by address const rollupId = ethers.id("rollup:demo"); const epoch = 7n; const dataCommitment = ethers.id("batch-data-commitment"); beforeEach(async function () { [admin, m1, m2, m3, outsider] = await ethers.getSigners(); // sort committee members ascending (attest requires ascending recovered addr) members = [m1, m2, m3].sort((a, b) => a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1 ); const F = await ethers.getContractFactory("AereDACommittee"); dac = await F.deploy(admin.address, members.map((m) => m.address), 2); await dac.waitForDeployment(); }); async function sign(wallet, rId = rollupId, ep = epoch, dc = dataCommitment) { // members sign the raw inner hash via personal_sign; the contract wraps with // the EIP-191 prefix in attestationDigest. const inner = ethers.keccak256( ethers.AbiCoder.defaultAbiCoder().encode( ["string", "uint256", "address", "bytes32", "uint256", "bytes32"], ["AereDACommittee:attest", 31337n, await dac.getAddress(), rId, ep, dc] ) ); return wallet.signMessage(ethers.getBytes(inner)); } it("constructs with the committee and threshold", async function () { expect(await dac.memberCount()).to.equal(3n); expect(await dac.threshold()).to.equal(2n); expect(await dac.isMember(members[0].address)).to.equal(true); expect(await dac.isMember(outsider.address)).to.equal(false); expect((await dac.getMembers()).length).to.equal(3); }); it("records availability with M distinct member signatures", async function () { const sigs = [await sign(members[0]), await sign(members[1])]; // ascending const tx = await dac.attest(rollupId, epoch, dataCommitment, sigs); await tx.wait(); expect(await dac.isAvailable(rollupId, epoch)).to.equal(true); const [available, dc, , n] = await dac.getAttestation(rollupId, epoch); expect(available).to.equal(true); expect(dc).to.equal(dataCommitment); expect(n).to.equal(2n); }); it("rejects M-1 signatures", async function () { const sigs = [await sign(members[0])]; await expect(dac.attest(rollupId, epoch, dataCommitment, sigs)) .to.be.revertedWithCustomError(dac, "ThresholdNotMet") .withArgs(1, 2); expect(await dac.isAvailable(rollupId, epoch)).to.equal(false); }); it("rejects a non-member signature", async function () { const sigs = [await sign(members[0]), await sign(outsider)]; // ordering may or may not place outsider second; either way it must revert. await expect(dac.attest(rollupId, epoch, dataCommitment, sigs)).to.be.reverted; expect(await dac.isAvailable(rollupId, epoch)).to.equal(false); }); it("rejects duplicate / unsorted signatures", async function () { const dup = await sign(members[0]); await expect(dac.attest(rollupId, epoch, dataCommitment, [dup, dup])) .to.be.revertedWithCustomError(dac, "UnsortedOrDuplicate"); }); it("rejects re-recording the same epoch", async function () { const sigs = [await sign(members[0]), await sign(members[1])]; await dac.attest(rollupId, epoch, dataCommitment, sigs); await expect(dac.attest(rollupId, epoch, dataCommitment, sigs)) .to.be.revertedWithCustomError(dac, "AlreadyRecorded"); }); it("admin governance cannot forge availability, and freeze locks it", async function () { // no admin function can set availability; only attest can. await dac.connect(admin).freeze(); expect(await dac.frozen()).to.equal(true); await expect(dac.connect(admin).addMember(outsider.address)).to.be.revertedWithCustomError(dac, "IsFrozen"); // attest still works post-freeze const sigs = [await sign(members[0]), await sign(members[1])]; await dac.attest(rollupId, epoch, dataCommitment, sigs); expect(await dac.isAvailable(rollupId, epoch)).to.equal(true); }); it("only admin can manage the committee", async function () { await expect(dac.connect(outsider).setThreshold(1)).to.be.revertedWithCustomError(dac, "NotAdmin"); }); });