aere-contracts/test/shutter-mempool-v2.test.js
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
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.
2026-07-20 01:02:37 +03:00

247 lines
12 KiB
JavaScript

// 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");
});
});