// shutter-mempool-v2.test.js // // Full PoC test for AereShutterMempoolV2: a REAL 3-of-5 threshold-BLS committee // generated by scripts/shutter-crypto.js (real ciphertexts, real shares, real // Lagrange reconstruction) with every pairing verified on-chain via the local // prague EIP-2537 pairing precompile (0x0f), the same precompile probed live on // AERE chain 2800. const { expect } = require("chai"); const { ethers } = require("hardhat"); const crypto = require("crypto"); const C = require("../scripts/shutter-crypto"); const T = 3, N = 5; function rand32() { return "0x" + crypto.randomBytes(32).toString("hex"); } function toHex(buf) { return "0x" + buf.toString("hex"); } // Build the encrypted envelopes + sequencer ordering (execution order == the // given permutation of submission order; default identity). function buildEnvelopes(com, epochId, plaintexts, order) { order = order || plaintexts.map((_, i) => i); const env = plaintexts.map((pt) => { const e = C.encrypt(com, epochId, pt); const opening = rand32(); const plaintextHex = toHex(pt); const commitment = C.commitmentOf(plaintextHex, opening); return { ciphertext: e.ciphertext, opening, plaintextHex, commitment, pt }; }); // execution position `pos` reveals env[order[pos]] const leaves = order.map((srcIdx, pos) => C.leafOf(epochId, pos, env[srcIdx].commitment)); const { root, proofs } = C.buildMerkle(leaves); return { env, order, root, proofs }; } async function deployAndRegister() { const [coordinator] = await ethers.getSigners(); const com = C.setupCommittee(T, N); const F = await ethers.getContractFactory("AereShutterMempoolV2"); const mempool = await F.deploy(coordinator.address, com.g2Gen); await mempool.waitForDeployment(); const keyperAddrs = Array.from({ length: N }, () => ethers.Wallet.createRandom().address); await (await mempool.registerKeypers(keyperAddrs, com.pubkeys, T)).wait(); return { mempool, com, coordinator, keyperAddrs }; } // Shares for keyper indices (0-based). x-coord = index+1. function shareFor(com, ep, keyperIndex) { return C.makeShare(com.shares[keyperIndex].secret, ep.point); } describe("AereShutterMempoolV2 (real threshold-BLS anti-MEV mempool)", function () { let ctx; before(async function () { this.timeout(120000); // EIP-2537 BLS12-381 precompiles (incl. pairing 0x0f) activate in PRAGUE. // The repo's default `hardhat` network is pinned to cancun (to preserve the // Falcon-1024 gas-cap behaviour), so run this suite under a prague config: // npx hardhat test test/shutter-mempool-v2.test.js --config hardhat.config.shutter-v2.js // If the local node lacks the precompile, skip cleanly instead of failing. const id = "0x" + [C.g1Hex(C.G1.BASE), C.g2Hex(C.G2.BASE), C.g1Hex(C.G1.BASE.negate()), C.g2Hex(C.G2.BASE)].map((h) => h.slice(2)).join(""); let ok = false; try { ok = BigInt(await ethers.provider.call({ to: "0x000000000000000000000000000000000000000f", data: id })) === 1n; } catch (_) {} if (!ok) { console.warn(" [skip] local node has no EIP-2537 pairing precompile (0x0f). Run with --config hardhat.config.shutter-v2.js (prague)."); this.skip(); } ctx = await deployAndRegister(); }); it("registers a 3-of-5 committee and reports it", async () => { expect(await ctx.mempool.committeeSet()).to.equal(true); expect(await ctx.mempool.threshold()).to.equal(T); expect(await ctx.mempool.keyperCount()).to.equal(N); }); it("full happy path: encrypt -> order -> 3 valid shares -> finalize -> reveal both in order", async function () { this.timeout(120000); const { mempool, com } = ctx; const epochId = 100; const ep = C.epochIdentity(epochId); const plaintexts = [ Buffer.from("transfer(0xAbCd, 100 AERE) nonce=7", "utf8"), Buffer.from("swap 5 USDC.e -> AERE minOut=4.98", "utf8"), ]; const built = buildEnvelopes(com, epochId, plaintexts); await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait(); for (const e of built.env) await (await mempool.submitEncrypted(epochId, e.ciphertext, e.commitment)).wait(); let info = await mempool.getEpoch(epochId); expect(info.phase).to.equal(1); // Open expect(info.envelopeCount).to.equal(2); // decryption share BEFORE ordering must revert (prevents early-decrypt MEV) const s0 = shareFor(com, ep, 0); await expect(mempool.submitDecryptionShare(epochId, 0, s0)) .to.be.revertedWithCustomError(mempool, "OrderingNotCommitted"); // sequencer commits the ordering await (await mempool.commitOrdering(epochId, built.root)).wait(); info = await mempool.getEpoch(epochId); expect(info.phase).to.equal(2); // Ordered // 3 valid shares (keypers 0,1,2 -> x = 1,2,3), each pairing-verified on-chain for (const k of [0, 1, 2]) { await (await mempool.submitDecryptionShare(epochId, k, shareFor(com, ep, k))).wait(); } info = await mempool.getEpoch(epochId); expect(info.shareCount).to.equal(3); // reconstruct DK off-chain via Lagrange from subset {1,2,3}; verify on-chain const { DK, point: dkPoint } = C.reconstructDK(ep.point, com, [1, 2, 3]); await (await mempool.finalizeEpoch(epochId, DK)).wait(); expect(await mempool.isFinalized(epochId)).to.equal(true); // sanity: DK actually decrypts the real ciphertexts to the original plaintexts for (let i = 0; i < plaintexts.length; i++) { const dec = C.decrypt(dkPoint, built.env[i].ciphertext); expect(dec.equals(plaintexts[i])).to.equal(true); } // reveal both, strictly in committed order for (let pos = 0; pos < built.order.length; pos++) { const src = built.order[pos]; const e = built.env[src]; await expect(mempool.revealAndExecute(epochId, pos, e.plaintextHex, e.opening, built.proofs[pos])) .to.emit(mempool, "Executed"); } const after = await mempool.getEpoch(epochId); expect(after.nextExecIndex).to.equal(2); }); it("rejects a share that is inconsistent with its VSS commitment (wrong share)", async () => { const { mempool, com } = ctx; const epochId = 101; const ep = C.epochIdentity(epochId); await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait(); const e = C.encrypt(com, epochId, Buffer.from("x", "utf8")); const commitment = C.commitmentOf("0x1234", rand32()); await (await mempool.submitEncrypted(epochId, e.ciphertext, commitment)).wait(); await (await mempool.commitOrdering(epochId, ethers.keccak256("0xabcd"))).wait(); // keyper 0's real share, but submitted AS keyper index 1 -> pairing must fail const wrong = shareFor(com, ep, 0); await expect(mempool.submitDecryptionShare(epochId, 1, wrong)) .to.be.revertedWithCustomError(mempool, "BadShare"); // a truncated / malformed point is also rejected await expect(mempool.submitDecryptionShare(epochId, 1, "0x1234")) .to.be.revertedWithCustomError(mempool, "BadPointLength"); // the correct share for keyper 1 passes await expect(mempool.submitDecryptionShare(epochId, 1, shareFor(com, ep, 1))) .to.emit(mempool, "ShareSubmitted"); }); it("rejects a forged/incorrect DK reconstruction at finalize", async () => { const { mempool, com } = ctx; const epochId = 102; const ep = C.epochIdentity(epochId); await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait(); const e = C.encrypt(com, epochId, Buffer.from("y", "utf8")); await (await mempool.submitEncrypted(epochId, e.ciphertext, C.commitmentOf("0x22", rand32()))).wait(); await (await mempool.commitOrdering(epochId, ethers.keccak256("0xbeef"))).wait(); for (const k of [0, 1, 2]) await (await mempool.submitDecryptionShare(epochId, k, shareFor(com, ep, k))).wait(); // a DK from a WRONG epoch's identity does not satisfy e(DK,g2)==e(H1,PK) const wrongEp = C.epochIdentity(999); const { DK: wrongDK } = C.reconstructDK(wrongEp.point, com, [1, 2, 3]); await expect(mempool.finalizeEpoch(epochId, wrongDK)) .to.be.revertedWithCustomError(mempool, "BadReconstruction"); // the correct DK finalizes const { DK } = C.reconstructDK(ep.point, com, [1, 2, 3]); await expect(mempool.finalizeEpoch(epochId, DK)).to.emit(mempool, "EpochFinalized"); }); it("reverts finalize with fewer than t shares", async () => { const { mempool, com } = ctx; const epochId = 103; const ep = C.epochIdentity(epochId); await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait(); await (await mempool.submitEncrypted(epochId, C.encrypt(com, epochId, Buffer.from("z")).ciphertext, C.commitmentOf("0x33", rand32()))).wait(); await (await mempool.commitOrdering(epochId, ethers.keccak256("0xcafe"))).wait(); for (const k of [0, 1]) await (await mempool.submitDecryptionShare(epochId, k, shareFor(com, ep, k))).wait(); // only 2 const { DK } = C.reconstructDK(ep.point, com, [1, 2, 3]); await expect(mempool.finalizeEpoch(epochId, DK)).to.be.revertedWithCustomError(mempool, "NotEnoughShares"); }); it("reverts revealAndExecute with a plaintext that does not open the commitment", async () => { const { mempool, com } = ctx; const epochId = 104; const ep = C.epochIdentity(epochId); const plaintexts = [Buffer.from("real-payload-A", "utf8"), Buffer.from("real-payload-B", "utf8")]; const built = buildEnvelopes(com, epochId, plaintexts); await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait(); for (const e of built.env) await (await mempool.submitEncrypted(epochId, e.ciphertext, e.commitment)).wait(); await (await mempool.commitOrdering(epochId, built.root)).wait(); for (const k of [0, 1, 2]) await (await mempool.submitDecryptionShare(epochId, k, shareFor(com, ep, k))).wait(); await (await mempool.finalizeEpoch(epochId, C.reconstructDK(ep.point, com, [1, 2, 3]).DK)).wait(); // tampered plaintext at position 0 with the real opening/proof -> commitment mismatch const tampered = toHex(Buffer.from("EVIL-payload-A", "utf8")); await expect(mempool.revealAndExecute(epochId, 0, tampered, built.env[0].opening, built.proofs[0])) .to.be.revertedWithCustomError(mempool, "UnknownCommitment"); // correct reveal at position 0 works await expect(mempool.revealAndExecute(epochId, 0, built.env[0].plaintextHex, built.env[0].opening, built.proofs[0])) .to.emit(mempool, "Executed"); }); it("reverts revealAndExecute out of committed order", async () => { const { mempool, com } = ctx; const epochId = 105; const ep = C.epochIdentity(epochId); const plaintexts = [Buffer.from("order-0", "utf8"), Buffer.from("order-1", "utf8")]; const built = buildEnvelopes(com, epochId, plaintexts); await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait(); for (const e of built.env) await (await mempool.submitEncrypted(epochId, e.ciphertext, e.commitment)).wait(); await (await mempool.commitOrdering(epochId, built.root)).wait(); for (const k of [0, 1, 2]) await (await mempool.submitDecryptionShare(epochId, k, shareFor(com, ep, k))).wait(); await (await mempool.finalizeEpoch(epochId, C.reconstructDK(ep.point, com, [1, 2, 3]).DK)).wait(); // trying to execute position 1 before position 0 must revert await expect(mempool.revealAndExecute(epochId, 1, built.env[1].plaintextHex, built.env[1].opening, built.proofs[1])) .to.be.revertedWithCustomError(mempool, "OutOfOrder"); // in-order execution then succeeds await (await mempool.revealAndExecute(epochId, 0, built.env[0].plaintextHex, built.env[0].opening, built.proofs[0])).wait(); await expect(mempool.revealAndExecute(epochId, 1, built.env[1].plaintextHex, built.env[1].opening, built.proofs[1])) .to.emit(mempool, "Executed"); }); it("flags a keyper whose alleged share is inconsistent with its commitment", async () => { const { mempool, com } = ctx; const epochId = 106; const ep = C.epochIdentity(epochId); await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait(); // report keyper 1 using keyper 0's share -> inconsistent -> flag emitted await expect(mempool.reportInconsistentShare(epochId, 1, shareFor(com, ep, 0))) .to.emit(mempool, "KeyperFlagged"); // a genuinely consistent share cannot be used to falsely accuse await expect(mempool.reportInconsistentShare(epochId, 1, shareFor(com, ep, 1))) .to.be.revertedWithCustomError(mempool, "ShareIsConsistent"); }); });