const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("AereShutterMempool", () => { let foundation, keyper, user, outsider; let m; const REVEAL_DELAY = 2; const KEYPER_SIZE = 13; const KEYPER_THRESHOLD = 9; const COMMITTEE_ROOT = ethers.keccak256(ethers.toUtf8Bytes("committee-v1")); const AGG_PUBKEY = ethers.keccak256(ethers.toUtf8Bytes("agg-pubkey-v1")); beforeEach(async () => { [foundation, keyper, user, outsider] = await ethers.getSigners(); const F = await ethers.getContractFactory("AereShutterMempool"); m = await F.deploy(foundation.address, REVEAL_DELAY, KEYPER_SIZE, KEYPER_THRESHOLD); await m.waitForDeployment(); }); async function mineBlocks(n) { for (let i = 0; i < n; i++) await ethers.provider.send("evm_mine", []); } it("only Foundation opens epochs; cannot reopen the same epoch", async () => { await expect( m.connect(user).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY) ).to.be.revertedWithCustomError(m, "NotFoundation"); await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY); expect((await m.epochs(1n)).exists).to.equal(true); await expect( m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY) ).to.be.revertedWithCustomError(m, "EpochClosedAlready"); }); it("opening a new epoch closes the previous one", async () => { await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY); await m.connect(foundation).openEpoch(2n, COMMITTEE_ROOT, AGG_PUBKEY); expect((await m.epochs(1n)).endBlock).to.be.gt(0n); expect((await m.epochs(2n)).endBlock).to.equal(0n); expect(await m.currentEpochId()).to.equal(2n); }); it("commit requires open epoch; envelope is recorded", async () => { const ch = ethers.keccak256(ethers.toUtf8Bytes("cipher-1")); await expect( m.connect(user).commit(ch, "ipfs://x") ).to.be.revertedWithCustomError(m, "EpochNotOpen"); await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY); const tx = await m.connect(user).commit(ch, "ipfs://x"); const rc = await tx.wait(); const ev = rc.logs.map(l => { try { return m.interface.parseLog(l); } catch { return null; } }).find(x => x?.name === 'Committed'); expect(ev).to.exist; const env = await m.envelopeOf(ev.args.envelopeId); expect(env.sender).to.equal(user.address); expect(env.ciphertextHash).to.equal(ch); expect(await m.envelopeCount(1n)).to.equal(1n); }); it("releaseDecryptionKey rejected before REVEAL_DELAY; succeeds after", async () => { await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY); const kh = ethers.keccak256(ethers.toUtf8Bytes("key")); await expect( m.connect(foundation).releaseDecryptionKey(1n, kh, "ipfs://key") ).to.be.revertedWithCustomError(m, "RevealTooEarly"); await mineBlocks(REVEAL_DELAY); await m.connect(foundation).releaseDecryptionKey(1n, kh, "ipfs://key"); expect(await m.isKeyReleased(1n)).to.equal(true); }); it("markRevealed: Foundation-only, plaintext-bound, idempotent (audit fix HIGH #3)", async () => { await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY); const plaintext = ethers.toUtf8Bytes("hello-world"); const releasedKey = ethers.keccak256(ethers.toUtf8Bytes("k")); const ch = ethers.keccak256(ethers.solidityPacked(["bytes", "bytes32"], [plaintext, releasedKey])); const tx = await m.connect(user).commit(ch, ""); const rc = await tx.wait(); const ev = rc.logs.map(l => { try { return m.interface.parseLog(l); } catch { return null; } }).find(x => x?.name === 'Committed'); const id = ev.args.envelopeId; // Non-Foundation can't markRevealed. await expect(m.connect(outsider).markRevealed(id, plaintext)).to.be.revertedWithCustomError(m, "NotFoundation"); await mineBlocks(REVEAL_DELAY); // Key not yet released. await expect(m.connect(foundation).markRevealed(id, plaintext)).to.be.revertedWithCustomError(m, "KeyNotReleased"); await m.connect(foundation).releaseDecryptionKey(1n, releasedKey, ""); // Plaintext-mismatch rejected. await expect(m.connect(foundation).markRevealed(id, ethers.toUtf8Bytes("WRONG"))).to.be.revertedWithCustomError(m, "PlaintextMismatch"); // Correct plaintext succeeds. await m.connect(foundation).markRevealed(id, plaintext); expect(await m.isRevealed(id)).to.equal(true); // Idempotent. await expect(m.connect(foundation).markRevealed(id, plaintext)).to.be.revertedWithCustomError(m, "AlreadyRevealed"); }); it("unknown envelope reveal reverts", async () => { const fake = ethers.keccak256(ethers.toUtf8Bytes("nonexistent")); await expect( m.connect(foundation).markRevealed(fake, "0x") ).to.be.revertedWithCustomError(m, "EnvelopeUnknown"); }); it("commit refuses to overwrite an existing envelope (audit fix HIGH #4)", async () => { await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY); const ch = ethers.keccak256(ethers.toUtf8Bytes("c1")); await m.connect(user).commit(ch, ""); await expect(m.connect(user).commit(ch, "")).to.be.revertedWithCustomError(m, "EnvelopeAlreadyCommitted"); }); it("releaseDecryptionKey one-shot per epoch (audit fix HIGH #5)", async () => { await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY); await mineBlocks(REVEAL_DELAY); const k = ethers.keccak256(ethers.toUtf8Bytes("k")); await m.connect(foundation).releaseDecryptionKey(1n, k, ""); await expect(m.connect(foundation).releaseDecryptionKey(1n, k, "")).to.be.revertedWithCustomError(m, "KeyAlreadyReleased"); }); it("constructor requires revealDelayBlocks >= 1 (audit fix MED #31)", async () => { const F = await ethers.getContractFactory("AereShutterMempool"); await expect(F.deploy(foundation.address, 0, KEYPER_SIZE, KEYPER_THRESHOLD)).to.be.revertedWith("reveal-delay-zero"); }); it("zero ciphertext hash rejected", async () => { await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY); await expect( m.connect(user).commit(ethers.ZeroHash, "") ).to.be.revertedWithCustomError(m, "EmptyCommitment"); }); });