// AerePQCEncryptedIntent - quantum-safe encrypted-intent settlement rail. // // A user hands a designated solver an order whose CONTENTS are hidden from front-runners until // settlement. The symmetric key protecting the intent is transported with ML-KEM-768 (FIPS 203), // so the confidentiality is post-quantum. On chain the contract holds only ciphertext HASHES, a // commitment, escrow and timeout logic; the heavy KEM/AEAD math runs off-chain. // // This suite drives the on-chain rail directly. It does NOT run real ML-KEM: the contract only // checks byte LENGTHS (ek = 1184, kem ciphertext = 1088) and HASHES, so fixed-length filler bytes // exercise every on-chain branch faithfully. Coverage: // - submit (happy path + every validation revert) // - honest settle (claim -> revealAndSettle with a matching reveal; escrow + bond paid to solver) // - solver-timeout refund (no-claim path, and claim-then-stall path where the bond is forfeited) // - reveal-mismatch (a wrong reveal reverts and the state is untouched; a later correct reveal wins) // // Run: npx hardhat test test/AerePQCEncryptedIntent.test.js const { expect } = require("chai"); const { ethers } = require("hardhat"); const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); const coder = ethers.AbiCoder.defaultAbiCoder(); const EK_LEN = 1184; // ML-KEM-768 encapsulation key const CT_LEN = 1088; // ML-KEM-768 ciphertext const HOUR = 3600; const DAY = 24 * HOUR; // deterministic filler bytes (keccak hash-chain), so tests are reproducible. function fill(n, seedText) { const out = new Uint8Array(n); let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText)); let i = 0; while (i < n) { seed = ethers.keccak256(seed); const chunk = ethers.getBytes(seed); for (let j = 0; j < chunk.length && i < n; j++, i++) out[i] = chunk[j]; } return ethers.hexlify(out); } // commitment == keccak256(abi.encode(intentPlaintext, salt)) - mirrors the contract exactly. function commitmentFor(plaintext, salt) { return ethers.keccak256(coder.encode(["bytes", "bytes32"], [plaintext, salt])); } describe("AerePQCEncryptedIntent (quantum-safe encrypted-intent rail)", function () { let c, user, solver, otherSolver, relayer, stranger; let ek, ekHash; // A canonical intent plaintext + fresh salt + its commitment, and the two ciphertext blobs. const PLAINTEXT = ethers.hexlify(ethers.toUtf8Bytes("SWAP 100 AERE -> USDC minOut=95 deadline=+1h")); const SALT = ethers.keccak256(ethers.toUtf8Bytes("intent-salt-0")); const COMMIT = commitmentFor(PLAINTEXT, SALT); const KEM_CT = fill(CT_LEN, "kem-ct"); const ENC_PAYLOAD = fill(96, "aead-blob"); // nonce(12)||ct||tag(16), any nonempty blob const ESCROW = ethers.parseEther("1"); const MIN_BOND = ethers.parseEther("0.25"); const CLAIM_WINDOW = 2 * HOUR; const SETTLE_WINDOW = 3 * HOUR; beforeEach(async function () { [user, solver, otherSolver, relayer, stranger] = await ethers.getSigners(); const F = await ethers.getContractFactory("AerePQCEncryptedIntent"); c = await F.deploy(); await c.waitForDeployment(); ek = fill(EK_LEN, "solver-ek-0"); ekHash = ethers.keccak256(ek); await (await c.connect(solver).publishEncapKey(ek)).wait(); }); // Submit a standard intent from `user` to `solver`; returns the assigned intentId. async function submitStd(overrides = {}) { const o = { solver: solver.address, ekHash, kemCt: KEM_CT, payload: ENC_PAYLOAD, commit: COMMIT, claimWindow: CLAIM_WINDOW, settleWindow: SETTLE_WINDOW, minBond: MIN_BOND, escrow: ESCROW, ...overrides, }; const id = await c.nextIntentId(); await ( await c .connect(user) .submitIntent(o.solver, o.ekHash, o.kemCt, o.payload, o.commit, o.claimWindow, o.settleWindow, o.minBond, { value: o.escrow, }) ).wait(); return id; } // ========================================================================= // Solver key registry // ========================================================================= describe("encapsulation key registry", function () { it("publishes a key, stores its hash, and emits the full key", async function () { const sk = await c.solverKeyOf(solver.address); expect(sk.keyHash).to.equal(ekHash); expect(sk.epoch).to.equal(1n); expect(sk.active).to.equal(true); }); it("rejects a wrong-length encapsulation key", async function () { await expect(c.connect(otherSolver).publishEncapKey(fill(EK_LEN - 1, "short"))) .to.be.revertedWithCustomError(c, "BadEncapKeyLength"); }); it("rotation bumps the epoch and changes the bound hash", async function () { const ek2 = fill(EK_LEN, "solver-ek-1"); await (await c.connect(solver).publishEncapKey(ek2)).wait(); const sk = await c.solverKeyOf(solver.address); expect(sk.epoch).to.equal(2n); expect(sk.keyHash).to.equal(ethers.keccak256(ek2)); // an intent that binds the OLD hash is now stale await expect(submitStd({ ekHash })).to.be.revertedWithCustomError(c, "StaleEncapKey"); }); it("retire blocks new intents but does not touch existing ones", async function () { const id = await submitStd(); await (await c.connect(solver).retireEncapKey()).wait(); await expect(submitStd()).to.be.revertedWithCustomError(c, "SolverKeyInactive"); // the already-open intent can still be claimed + settled await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait(); await (await c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT)).wait(); expect(await c.statusOf(id)).to.equal(3n); // Settled }); }); // ========================================================================= // submit validation // ========================================================================= describe("submitIntent validation", function () { it("stores hashes + commitment and locks the escrow; emits IntentSubmitted", async function () { const id = await c.nextIntentId(); await expect( c .connect(user) .submitIntent(solver.address, ekHash, KEM_CT, ENC_PAYLOAD, COMMIT, CLAIM_WINDOW, SETTLE_WINDOW, MIN_BOND, { value: ESCROW, }) ) .to.emit(c, "IntentSubmitted") .withArgs(id, user.address, solver.address, ekHash, COMMIT, ESCROW, MIN_BOND, anyValue, KEM_CT, ENC_PAYLOAD); const it = await c.getIntent(id); expect(it.user).to.equal(user.address); expect(it.solver).to.equal(solver.address); expect(it.status).to.equal(1n); // Open expect(it.escrow).to.equal(ESCROW); expect(it.kemCtHash).to.equal(ethers.keccak256(KEM_CT)); expect(it.payloadHash).to.equal(ethers.keccak256(ENC_PAYLOAD)); expect(it.commitment).to.equal(COMMIT); expect(await ethers.provider.getBalance(await c.getAddress())).to.equal(ESCROW); }); it("rejects an inactive/unknown solver", async function () { await expect(submitStd({ solver: otherSolver.address })) .to.be.revertedWithCustomError(c, "SolverKeyInactive"); }); it("rejects a stale ekHash", async function () { await expect(submitStd({ ekHash: ethers.ZeroHash })) .to.be.revertedWithCustomError(c, "StaleEncapKey"); }); it("rejects a wrong-length KEM ciphertext", async function () { await expect(submitStd({ kemCt: fill(CT_LEN - 1, "badct") })) .to.be.revertedWithCustomError(c, "BadCiphertextLength"); }); it("rejects an empty payload", async function () { await expect(submitStd({ payload: "0x" })).to.be.revertedWithCustomError(c, "EmptyPayload"); }); it("rejects a zero commitment", async function () { await expect(submitStd({ commit: ethers.ZeroHash })).to.be.revertedWithCustomError(c, "ZeroCommitment"); }); it("rejects out-of-range claim/settle windows", async function () { await expect(submitStd({ claimWindow: 8 * DAY })).to.be.revertedWithCustomError(c, "ClaimWindowOutOfRange"); await expect(submitStd({ claimWindow: 0 })).to.be.revertedWithCustomError(c, "ClaimWindowOutOfRange"); await expect(submitStd({ settleWindow: 8 * DAY })).to.be.revertedWithCustomError(c, "SettleWindowOutOfRange"); await expect(submitStd({ settleWindow: 0 })).to.be.revertedWithCustomError(c, "SettleWindowOutOfRange"); }); it("commitmentFor view matches the off-chain derivation", async function () { expect(await c.commitmentFor(PLAINTEXT, SALT)).to.equal(COMMIT); }); }); // ========================================================================= // Honest settle: submit -> claim -> revealAndSettle // ========================================================================= describe("honest settle", function () { it("solver claims, reveals the committed intent, and receives escrow + returned bond", async function () { const id = await submitStd(); // only the designated solver may claim await expect(c.connect(stranger).claimIntent(id, { value: MIN_BOND })) .to.be.revertedWithCustomError(c, "NotDesignatedSolver"); // bond below minimum is rejected await expect(c.connect(solver).claimIntent(id, { value: MIN_BOND - 1n })) .to.be.revertedWithCustomError(c, "InsufficientBond"); const bond = ethers.parseEther("0.4"); // >= minBond await expect(c.connect(solver).claimIntent(id, { value: bond })).to.emit(c, "IntentClaimed"); let it = await c.getIntent(id); expect(it.status).to.equal(2n); // Claimed expect(it.bond).to.equal(bond); // settle with the correct reveal; measure the solver's net gain (escrow + bond) net of gas. const before = await ethers.provider.getBalance(solver.address); const tx = await c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT); const rc = await tx.wait(); const after = await ethers.provider.getBalance(solver.address); expect(after - before).to.equal(ESCROW + bond - rc.fee); it = await c.getIntent(id); expect(it.status).to.equal(3n); // Settled expect(it.escrow).to.equal(0n); expect(it.bond).to.equal(0n); // contract fully drained for this intent expect(await ethers.provider.getBalance(await c.getAddress())).to.equal(0n); }); it("emits IntentSettled carrying the now-public plaintext", async function () { const id = await submitStd(); await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait(); await expect(c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT)) .to.emit(c, "IntentSettled") .withArgs(id, solver.address, ESCROW, PLAINTEXT); }); it("works with zero escrow and zero minBond", async function () { const id = await submitStd({ escrow: 0n, minBond: 0n }); await (await c.connect(solver).claimIntent(id, { value: 0 })).wait(); await (await c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT)).wait(); expect(await c.statusOf(id)).to.equal(3n); }); it("rejects settle before claim (must be Claimed)", async function () { const id = await submitStd(); await expect(c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT)) .to.be.revertedWithCustomError(c, "WrongStatus"); }); it("only the designated solver may settle", async function () { const id = await submitStd(); await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait(); await expect(c.connect(stranger).revealAndSettle(id, PLAINTEXT, SALT)) .to.be.revertedWithCustomError(c, "NotDesignatedSolver"); }); it("cannot claim after the claim window closes", async function () { const id = await submitStd(); await time.increase(CLAIM_WINDOW + 60); await expect(c.connect(solver).claimIntent(id, { value: MIN_BOND })) .to.be.revertedWithCustomError(c, "ClaimWindowClosed"); }); it("cannot settle after the settle window closes", async function () { const id = await submitStd(); await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait(); await time.increase(SETTLE_WINDOW + 60); await expect(c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT)) .to.be.revertedWithCustomError(c, "SettleWindowClosed"); }); it("cannot double-claim a claimed intent", async function () { const id = await submitStd(); await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait(); await expect(c.connect(solver).claimIntent(id, { value: MIN_BOND })) .to.be.revertedWithCustomError(c, "WrongStatus"); }); }); // ========================================================================= // Solver-timeout refund // ========================================================================= describe("solver-timeout refund", function () { it("no-claim: after the claim window the user reclaims the full escrow", async function () { const id = await submitStd(); // too early await expect(c.connect(relayer).timeoutRefund(id)).to.be.revertedWithCustomError(c, "NotYetRefundable"); expect(await c.isRefundable(id)).to.equal(false); await time.increase(CLAIM_WINDOW + 60); expect(await c.isRefundable(id)).to.equal(true); // permissionless poke; the refund always goes to the user (not the caller) await expect(c.connect(relayer).timeoutRefund(id)).to.changeEtherBalances( [user, await c.getAddress()], [ESCROW, -ESCROW] ); expect(await c.statusOf(id)).to.equal(4n); // Refunded // cannot refund twice await expect(c.connect(relayer).timeoutRefund(id)).to.be.revertedWithCustomError(c, "WrongStatus"); }); it("claim-then-stall: the solver's bond is forfeited to the user with the escrow", async function () { const id = await submitStd(); const bond = ethers.parseEther("0.5"); await (await c.connect(solver).claimIntent(id, { value: bond })).wait(); // still not refundable during the settle window await time.increase(SETTLE_WINDOW - 60); await expect(c.connect(relayer).timeoutRefund(id)).to.be.revertedWithCustomError(c, "NotYetRefundable"); // past the settle deadline: user gets escrow + forfeited bond, contract drains to 0 await time.increase(120); await expect(c.connect(relayer).timeoutRefund(id)) .to.emit(c, "IntentRefunded") .withArgs(id, user.address, ESCROW, bond); await expect(c.connect(relayer).timeoutRefund(id)).to.be.revertedWithCustomError(c, "WrongStatus"); // already refunded expect(await ethers.provider.getBalance(await c.getAddress())).to.equal(0n); }); it("timeoutRefund reverts for an unknown intent", async function () { await expect(c.connect(relayer).timeoutRefund(999)).to.be.revertedWithCustomError(c, "UnknownIntent"); }); }); // ========================================================================= // Reveal mismatch // ========================================================================= describe("reveal mismatch", function () { it("a wrong plaintext is rejected and the intent is untouched; the correct reveal then settles", async function () { const id = await submitStd(); await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait(); const wrongPlaintext = ethers.hexlify(ethers.toUtf8Bytes("SWAP 100 AERE -> USDC minOut=1 (tampered)")); await expect(c.connect(solver).revealAndSettle(id, wrongPlaintext, SALT)) .to.be.revertedWithCustomError(c, "CommitmentMismatch"); // still Claimed, escrow intact let it = await c.getIntent(id); expect(it.status).to.equal(2n); expect(it.escrow).to.equal(ESCROW); // a wrong SALT with the right plaintext is also rejected await expect(c.connect(solver).revealAndSettle(id, PLAINTEXT, ethers.keccak256(ethers.toUtf8Bytes("wrong-salt")))) .to.be.revertedWithCustomError(c, "CommitmentMismatch"); // the correct reveal still works (retry within the window) await (await c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT)).wait(); it = await c.getIntent(id); expect(it.status).to.equal(3n); // Settled }); }); });