Some checks failed
contracts-ci / Install (lockfile) → compile → full test suite (push) Has been cancelled
contracts-ci / Ethereum interop (EIP-2537 BLS, prague hardfork) (push) Has been cancelled
contracts-ci / PQC known-answer tests (NIST vectors) (push) Has been cancelled
contracts-ci / Coverage (scoped, with artifacts) (push) Has been cancelled
The published line and the local line had no common ancestor: the public one carried the redaction pass, the local one carried three weeks of corrections that never shipped. This commit ports the local work onto the public line, keeps every public redaction, and extends the same discretion to seven client mentions that were still named in published comments. Carried: LICENSE year and LICENSING.md; the measured burn figures replacing the deflation claim (the vault holds ~0.137 AERE of 2.8 billion, and burn is a share of validator coinbase revenue, which is zero today); 'audited' removed from next to Bouncy Castle; citation paths rewritten to published form with CITATIONS-UNRESOLVED.md remeasured 2026-08-11; VERIFY-POLICY.md; slashing and ownership comments brought down to what the code does; the AerePyth repair; the shutter test helper the tests cite; runnable package.json entries; the CI file split into a GitHub/Gitea twin pair with a real measured test-run status; and the .gitignore hardening written after a compiled artifact leaked a local path in a sibling repository. A false '2-of-3 multisig' description of the owner account is corrected to what the chain measures: an externally owned account. The self-audit findings catalog stays unpublished pending an explicit decision.
247 lines
12 KiB
JavaScript
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 test/helpers/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("./helpers/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");
|
|
});
|
|
});
|