// ============================================================================= // PQC-authorized intents: Falcon/ML-DSA-signed ERC-7683 cross-chain orders. // // Under test: // contracts/pqc/AerePQCOrderAuthorizer.sol (crypto-agility CONSUMER over the registry) // contracts/intents/AereSpokePool.sol (openForPQC + bindPQCKey PQC intent path) // contracts/intents/IAerePQCOrder.sol (PQCGaslessOrder + authorizer interface) // // The order is authorized ONLY by a NIST post-quantum signature (Falcon-512/1024, // ML-DSA-44, SLH-DSA-128s) over the EIP-712 order digest, verified on-chain by AERE's // live native PQC precompiles routed through the governed AereCryptoRegistry. So the // intent is quantum-durable end to end, and a Foundation scheme swap migrates the // accepted scheme with NO pool redeploy (proven below). // // TWO verification backends at 0x0AE1..0x0AE4 (Hardhat has no native PQC precompile): // A. MockPQCPrecompile — parses the input at the precompiles' SPEC offsets and accepts // iff the embedded commitment == keccak256(pk || message). Drives the accept / // bad-signature / wrong-scheme / replay / agility LOGIC deterministically. Because // the registry builds the SAME wire format the live precompiles parse, a positive // result proves correct routing/encoding (NOT the lattice cryptography). // B. RealFalcon512PrecompileAdapter — routes the staticcall into the NIST-KAT-proven // AereFalcon512Verifier, so a GENUINE @noble/post-quantum Falcon-512 signature over // the pool's real order digest fills the order, and the official NIST KAT vector 0 // is accepted. This proves the real crypto leg. // // Run: npx hardhat test test/pqc-intents.test.js // No em-dashes anywhere in this file. // ============================================================================= const { expect } = require("chai"); const { ethers, network } = require("hardhat"); const path = require("path"); const { pathToFileURL } = require("url"); const AbiCoder = ethers.AbiCoder.defaultAbiCoder(); const AERE_ORDER_DATA_TYPE = ethers.keccak256(ethers.toUtf8Bytes("AereV3Order")); const DEST_CHAIN = 1n; // Ethereum-like destination const Status = { UNKNOWN: 0, ACTIVE: 1, DEPRECATED: 2, REVOKED: 3 }; const PRECOMPILE = { 1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), 2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"), 3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), 4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"), }; const META = { 1: { name: "Falcon-512", pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, sigLen: 0, falcon: true }, 2: { name: "Falcon-1024", pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, sigLen: 0, falcon: true }, 3: { name: "ML-DSA-44", pkLen: 1312, esigHeader: 0x00, sigLen: 2420, falcon: false }, 4: { name: "SLH-DSA-128s", pkLen: 32, esigHeader: 0x00, sigLen: 7856, falcon: false }, }; // Deterministic seeded RNG (keccak hash-chain), identical to the hybrid-authorizer suite. function makeRng(seedText) { let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText)); function bytes(n) { const out = new Uint8Array(n); 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 out; } return { bytes }; } function makePubKey(scheme, rng) { const m = META[scheme]; const raw = rng.bytes(m.pkLen); if (m.falcon) raw[0] = m.pkHeader; return ethers.hexlify(raw); } // A "genuine" mock signature: the precompile accepts iff commitment == keccak256(pk||message), // where `message` is the 32-byte order digest the registry passes as messageHash. function genuineMockSig(scheme, pubKey, digest, rng) { const m = META[scheme]; const commitment = ethers.keccak256(ethers.concat([pubKey, digest])); if (m.falcon) { const nonce = rng.bytes(40); const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]); // 33 bytes return ethers.hexlify(ethers.concat([nonce, esig])); } const sig = new Uint8Array(m.sigLen); sig.set(ethers.getBytes(commitment), 0); return ethers.hexlify(sig); } function tamperSig(scheme, sigHex) { const b = ethers.getBytes(sigHex); const idx = META[scheme].falcon ? 41 : 0; // first byte of the embedded commitment b[idx] ^= 0x01; return ethers.hexlify(b); } function encodeOrderData(inputToken, inputAmount, outputToken, outputAmount, recipientB32, destChain) { return AbiCoder.encode( ["address", "uint256", "address", "uint256", "bytes32", "uint64"], [inputToken, inputAmount, outputToken, outputAmount, recipientB32, destChain] ); } async function latestTs() { return Number((await ethers.provider.getBlock("latest")).timestamp); } describe("PQC-authorized intents (Falcon/ML-DSA ERC-7683 orders)", function () { this.timeout(0); let signers, chainId; let mockCode; before(async function () { signers = await ethers.getSigners(); chainId = (await ethers.provider.getNetwork()).chainId; // Deploy the four precompile-format mocks once; capture their runtime code. mockCode = {}; const F = await ethers.getContractFactory("MockPQCPrecompile"); for (const scheme of [1, 2, 3, 4]) { const mock = await F.deploy(scheme); await mock.waitForDeployment(); mockCode[scheme] = await ethers.provider.getCode(await mock.getAddress()); } }); async function installMocks() { for (const scheme of [1, 2, 3, 4]) { await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], mockCode[scheme]]); } } async function deployStack(defaultId = 1) { const deployer = signers[0]; const reg = await (await ethers.getContractFactory("AereCryptoRegistry")).deploy(); await reg.waitForDeployment(); await reg.seedLiveSchemes(); // ids 1..5 = Falcon-512/1024, ML-DSA-44, SLH-DSA-128s, SHAKE256 const auth = await (await ethers.getContractFactory("AerePQCOrderAuthorizer")).deploy( await reg.getAddress(), defaultId ); await auth.waitForDeployment(); const ERC = await ethers.getContractFactory("MockERC20Lending"); const input = await ERC.deploy("USD Coin", "USDC", 6); await input.waitForDeployment(); const waere = await ERC.deploy("Wrapped AERE", "WAERE", 18); // dummy bond token for the constructor await waere.waitForDeployment(); const sink = await (await ethers.getContractFactory("MockFlushSink")).deploy(); await sink.waitForDeployment(); const pool = await (await ethers.getContractFactory("contracts/intents/AereSpokePool.sol:AereSpokePool")).deploy( await waere.getAddress(), await sink.getAddress() ); await pool.waitForDeployment(); await (await pool.setPQCAuthorizer(await auth.getAddress())).wait(); return { deployer, reg, auth, input, waere, sink, pool, poolAddr: await pool.getAddress(), inputAddr: await input.getAddress(), waereAddr: await waere.getAddress(), sinkAddr: await sink.getAddress(), }; } // Build the PQCGaslessOrder tuple (struct field order) for the ethers call. function buildOrder(ctx, { user, nonce, algorithmId, inputAmount, recipient, life = 3600, openLife = 1800 }) { const orderData = encodeOrderData(ctx.inputAddr, inputAmount, ctx.waereAddr, inputAmount, ethers.zeroPadValue(recipient, 32), DEST_CHAIN); return { tuple: [user, nonce, chainId, ctx._now + openLife, ctx._now + life, algorithmId, AERE_ORDER_DATA_TYPE, orderData], orderData, inputAmount, }; } async function prepUser(ctx, user, pubKey, amount) { await (await ctx.input.mint(user.address, amount)).wait(); await (await ctx.input.connect(user).approve(ctx.poolAddr, amount)).wait(); await (await ctx.pool.connect(user).bindPQCKey(pubKey)).wait(); } // ------------------------------------------------------------------------- // A. Mock-precompile LOGIC paths (routing / policy / replay / agility). // ------------------------------------------------------------------------- describe("mock precompile: accept / reject / wrong-scheme / replay", function () { let ctx; beforeEach(async function () { await installMocks(); ctx = await deployStack(1); ctx._now = await latestTs(); }); it("valid Falcon-512 order FILLS: locks solver principal, routes fee, bumps nonce", async function () { const [_, user, relayer, recipient] = signers; const rng = makeRng("valid-falcon"); const pubKey = makePubKey(1, rng); const inputAmount = 1000n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount * 2n); const { tuple } = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 1, inputAmount, recipient: recipient.address }); const digest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); const sig = genuineMockSig(1, pubKey, digest, rng); const rc = await (await ctx.pool.connect(relayer).openForPQC(tuple, pubKey, sig)).wait(); const openedId = rc.logs.find((l) => l.fragment && l.fragment.name === "Opened").args.orderId; const pqcEv = rc.logs.find((l) => l.fragment && l.fragment.name === "PQCOrderOpened"); // The PQC event's orderId is the SAME canonical id _openInternal stored. expect(pqcEv.args.orderId).to.equal(openedId); expect(pqcEv.args.user).to.equal(user.address); expect(pqcEv.args.algorithmId).to.equal(1n); // The order is really stored and open under that id. const stored = await ctx.pool.orders(openedId); expect(stored.open).to.equal(true); expect(stored.user).to.equal(user.address); const fee = (inputAmount * 50n) / 10_000n; const solverPortion = inputAmount - fee; expect(await ctx.input.balanceOf(ctx.poolAddr)).to.equal(solverPortion); expect(await ctx.input.balanceOf(ctx.sinkAddr)).to.equal(fee); expect(await ctx.pool.totalLocked(ctx.inputAddr)).to.equal(solverPortion); expect(await ctx.pool.pqcNonce(user.address)).to.equal(1n); }); it("valid ML-DSA-44 order FILLS (multi-scheme, explicit algorithmId=3)", async function () { const [_, user, relayer, recipient] = signers; const rng = makeRng("valid-mldsa"); const pubKey = makePubKey(3, rng); const inputAmount = 500n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount); const { tuple } = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 3, inputAmount, recipient: recipient.address }); const digest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); const sig = genuineMockSig(3, pubKey, digest, rng); await expect(ctx.pool.connect(relayer).openForPQC(tuple, pubKey, sig)).to.emit(ctx.pool, "PQCOrderOpened"); expect(await ctx.pool.pqcNonce(user.address)).to.equal(1n); }); it("algorithmId=0 routes to the authorizer's governed default (Falcon-512)", async function () { const [_, user, relayer, recipient] = signers; const rng = makeRng("default-algo"); const pubKey = makePubKey(1, rng); const inputAmount = 100n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount); const { tuple } = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 0, inputAmount, recipient: recipient.address }); const digest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); const sig = genuineMockSig(1, pubKey, digest, rng); await expect(ctx.pool.connect(relayer).openForPQC(tuple, pubKey, sig)).to.emit(ctx.pool, "PQCOrderOpened"); }); it("BAD SIGNATURE order is REJECTED (tampered commitment)", async function () { const [_, user, relayer, recipient] = signers; const rng = makeRng("bad-sig"); const pubKey = makePubKey(1, rng); const inputAmount = 100n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount); const { tuple } = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 1, inputAmount, recipient: recipient.address }); const digest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); const bad = tamperSig(1, genuineMockSig(1, pubKey, digest, rng)); await expect(ctx.pool.connect(relayer).openForPQC(tuple, pubKey, bad)) .to.be.revertedWithCustomError(ctx.pool, "InvalidSignature"); // Nothing moved: no lock, no nonce bump. expect(await ctx.pool.totalLocked(ctx.inputAddr)).to.equal(0n); expect(await ctx.pool.pqcNonce(user.address)).to.equal(0n); }); it("WRONG SCHEME is REJECTED: a Falcon key/sig declared under the ML-DSA id", async function () { const [_, user, relayer, recipient] = signers; const rng = makeRng("wrong-scheme"); const pubKey = makePubKey(1, rng); // 897-byte Falcon key, bound const inputAmount = 100n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount); // Declare algorithmId = 3 (ML-DSA-44): the registry routes to a verifier expecting a // 1312-byte key, so the 897-byte Falcon key fails the length gate -> not authorized. const { tuple } = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 3, inputAmount, recipient: recipient.address }); const digest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); const falconSig = genuineMockSig(1, pubKey, digest, rng); await expect(ctx.pool.connect(relayer).openForPQC(tuple, pubKey, falconSig)) .to.be.revertedWithCustomError(ctx.pool, "InvalidSignature"); }); it("WRONG SCHEME is REJECTED: a hash-only scheme id (SHAKE256 id 5) authorizes nothing", async function () { const [_, user, relayer, recipient] = signers; const rng = makeRng("hash-only"); const pubKey = makePubKey(1, rng); const inputAmount = 100n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount); const { tuple } = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 5, inputAmount, recipient: recipient.address }); const digest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); const sig = genuineMockSig(1, pubKey, digest, rng); await expect(ctx.pool.connect(relayer).openForPQC(tuple, pubKey, sig)) .to.be.revertedWithCustomError(ctx.pool, "InvalidSignature"); }); it("REPLAY is REJECTED: the same order cannot be opened twice (nonce consumed)", async function () { const [_, user, relayer, recipient] = signers; const rng = makeRng("replay"); const pubKey = makePubKey(1, rng); const inputAmount = 100n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount * 3n); const { tuple } = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 1, inputAmount, recipient: recipient.address }); const digest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); const sig = genuineMockSig(1, pubKey, digest, rng); await (await ctx.pool.connect(relayer).openForPQC(tuple, pubKey, sig)).wait(); expect(await ctx.pool.pqcNonce(user.address)).to.equal(1n); // Exact-replay of the captured (order, sig): nonce 0 no longer current. await expect(ctx.pool.connect(relayer).openForPQC(tuple, pubKey, sig)) .to.be.revertedWithCustomError(ctx.pool, "NonceMismatch"); // Even a freshly-signed order at the stale nonce 0 is rejected. const sig2 = genuineMockSig(1, pubKey, digest, makeRng("replay-2")); await expect(ctx.pool.connect(relayer).openForPQC(tuple, pubKey, sig2)) .to.be.revertedWithCustomError(ctx.pool, "NonceMismatch"); }); it("REJECTS an unbound funder and a pubkey that does not match the bound commitment", async function () { const [_, user, relayer, recipient, stranger] = signers; const rng = makeRng("binding"); const pubKey = makePubKey(1, rng); const otherKey = makePubKey(1, makeRng("binding-other")); const inputAmount = 100n * 10n ** 6n; // Unbound: mint + approve but never bindPQCKey. await (await ctx.input.mint(user.address, inputAmount)).wait(); await (await ctx.input.connect(user).approve(ctx.poolAddr, inputAmount)).wait(); const { tuple } = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 1, inputAmount, recipient: recipient.address }); const digest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); const sig = genuineMockSig(1, pubKey, digest, rng); await expect(ctx.pool.connect(relayer).openForPQC(tuple, pubKey, sig)) .to.be.revertedWithCustomError(ctx.pool, "PQCKeyNotBound"); // Bind pubKey, but present a DIFFERENT key at open -> rejected before any verify. await (await ctx.pool.connect(user).bindPQCKey(pubKey)).wait(); await expect(ctx.pool.connect(relayer).openForPQC(tuple, otherKey, genuineMockSig(1, otherKey, digest, rng))) .to.be.revertedWithCustomError(ctx.pool, "PQCKeyNotBound"); }); it("REJECTS an order for the wrong chain and after the open deadline", async function () { const [_, user, relayer, recipient] = signers; const rng = makeRng("deadline"); const pubKey = makePubKey(1, rng); const inputAmount = 100n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount); // Wrong origin chain id. const wrongChain = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 1, inputAmount, recipient: recipient.address }); wrongChain.tuple[2] = chainId + 1n; const d1 = await ctx.auth.orderDigest(ctx.poolAddr, wrongChain.tuple, pubKey); await expect(ctx.pool.connect(relayer).openForPQC(wrongChain.tuple, pubKey, genuineMockSig(1, pubKey, d1, rng))) .to.be.revertedWithCustomError(ctx.pool, "InvalidSignature"); // Past open deadline. const expired = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 1, inputAmount, recipient: recipient.address, openLife: -10 }); const d2 = await ctx.auth.orderDigest(ctx.poolAddr, expired.tuple, pubKey); await expect(ctx.pool.connect(relayer).openForPQC(expired.tuple, pubKey, genuineMockSig(1, pubKey, d2, rng))) .to.be.revertedWithCustomError(ctx.pool, "DeadlinePassed"); }); it("PQC path is disabled when the authorizer is unset", async function () { const [_, user, relayer, recipient] = signers; const rng = makeRng("unset"); const pubKey = makePubKey(1, rng); const inputAmount = 100n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount); await (await ctx.pool.setPQCAuthorizer(ethers.ZeroAddress)).wait(); const { tuple } = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 1, inputAmount, recipient: recipient.address }); const digest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); await expect(ctx.pool.connect(relayer).openForPQC(tuple, pubKey, genuineMockSig(1, pubKey, digest, rng))) .to.be.revertedWithCustomError(ctx.pool, "PQCAuthorizerUnset"); }); }); // ------------------------------------------------------------------------- // THE HEADLINE: a Foundation scheme swap on the REGISTRY changes the accepted // PQC scheme for the SAME requested id, with NO pool or authorizer redeploy. // ------------------------------------------------------------------------- describe("zero-redeploy scheme migration (Falcon-512 -> ML-DSA-44 successor)", function () { let ctx; beforeEach(async function () { await installMocks(); ctx = await deployStack(1); ctx._now = await latestTs(); }); it("the same algorithmId=1 stops accepting Falcon-512 and starts accepting the ML-DSA-44 successor", async function () { const [_, user, relayer, recipient] = signers; const rng = makeRng("swap"); const inputAmount = 100n * 10n ** 6n; // BEFORE: a Falcon-512 key/order under id 1 fills. const fkey = makePubKey(1, rng); await prepUser(ctx, user, fkey, inputAmount * 4n); const beforeOrder = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 1, inputAmount, recipient: recipient.address }); const dBefore = await ctx.auth.orderDigest(ctx.poolAddr, beforeOrder.tuple, fkey); await expect(ctx.pool.connect(relayer).openForPQC(beforeOrder.tuple, fkey, genuineMockSig(1, fkey, dBefore, rng))) .to.emit(ctx.pool, "PQCOrderOpened"); // Foundation-governed swap on the REGISTRY (no pool/authorizer redeploy): // 1. register an ML-DSA-44-shaped successor (ACTIVE), 2. point id 1 -> it, // 3. deprecate id 1. const tx = await ctx.reg.addAlgorithm("ML-DSA-44-succ", 3, PRECOMPILE[3], 1312, 2420, 0x00, 351000, true); const rc = await tx.wait(); const newId = Number(rc.logs.map((l) => ctx.reg.interface.parseLog(l)).find((p) => p && p.name === "AlgorithmAdded").args.id); await (await ctx.reg.setSuccessor(1, newId)).wait(); await (await ctx.reg.setStatus(1, Status.DEPRECATED)).wait(); // AFTER: a Falcon-512 order under the SAME id 1 is now rejected (routes to the // ML-DSA successor, whose verifier rejects the 897-byte Falcon key). const fkey2 = makePubKey(1, makeRng("swap-falcon2")); await (await ctx.pool.connect(user).bindPQCKey(fkey2)).wait(); const afterFalcon = buildOrder(ctx, { user: user.address, nonce: 1, algorithmId: 1, inputAmount, recipient: recipient.address }); const dF = await ctx.auth.orderDigest(ctx.poolAddr, afterFalcon.tuple, fkey2); await expect(ctx.pool.connect(relayer).openForPQC(afterFalcon.tuple, fkey2, genuineMockSig(1, fkey2, dF, rng))) .to.be.revertedWithCustomError(ctx.pool, "InvalidSignature"); // An ML-DSA-44 order under the SAME requested id 1 now fills. const mkey = makePubKey(3, makeRng("swap-mldsa")); await (await ctx.pool.connect(user).bindPQCKey(mkey)).wait(); const afterMldsa = buildOrder(ctx, { user: user.address, nonce: 1, algorithmId: 1, inputAmount, recipient: recipient.address }); const dM = await ctx.auth.orderDigest(ctx.poolAddr, afterMldsa.tuple, mkey); await expect(ctx.pool.connect(relayer).openForPQC(afterMldsa.tuple, mkey, genuineMockSig(3, mkey, dM, rng))) .to.emit(ctx.pool, "PQCOrderOpened"); expect(await ctx.pool.pqcNonce(user.address)).to.equal(2n); }); }); // ------------------------------------------------------------------------- // B. REAL Falcon-512 crypto: genuine @noble signatures + the NIST KAT vector, // verified by the KAT-proven AereFalcon512Verifier behind a precompile adapter. // ------------------------------------------------------------------------- describe("real Falcon-512 (KAT-proven verifier, genuine signatures over the order digest)", function () { let falcon512, adapterCode, verifier; const kat = require("./falcon512_kat0.json"); const HEAVY = { gasLimit: 500_000_000 }; before(async function () { const nobleFalcon = path.resolve(__dirname, "../../sdk-js/node_modules/@noble/post-quantum/falcon.js"); ({ falcon512 } = await import(pathToFileURL(nobleFalcon).href)); verifier = await (await ethers.getContractFactory("AereFalcon512Verifier")).deploy(); await verifier.waitForDeployment(); const adapter = await (await ethers.getContractFactory("RealFalcon512PrecompileAdapter")).deploy(await verifier.getAddress()); await adapter.waitForDeployment(); adapterCode = await ethers.provider.getCode(await adapter.getAddress()); }); // noble detached (0x39 || nonce(40) || compressed) -> AERE envelope (nonce(40) || 0x29 || compressed) function realEnvelope(detached) { const nonce = detached.slice(1, 41); const compressed = detached.slice(41); const esig = new Uint8Array(1 + compressed.length); esig[0] = 0x29; esig.set(compressed, 1); return ethers.hexlify(ethers.concat([nonce, esig])); } async function installRealFalcon() { await network.provider.send("hardhat_setCode", [PRECOMPILE[1], adapterCode]); } it("the precompile adapter accepts the OFFICIAL NIST Falcon-512 KAT vector 0", async function () { await installRealFalcon(); const ret = await ethers.provider.call({ to: PRECOMPILE[1], data: ethers.concat([kat.pk, kat.sm]) }); expect(BigInt(ret)).to.equal(1n); }); it("a GENUINE Falcon-512 signature over the pool's order digest FILLS the order; a tampered one is rejected", async function () { await installRealFalcon(); const [_, user, relayer, recipient] = signers; const ctx = await deployStack(1); ctx._now = await latestTs(); // Falcon keygen wants a 48-byte seed; derive one deterministically. const seed = ethers.getBytes( ethers.concat([ ethers.keccak256(ethers.toUtf8Bytes("pqc-order-falcon-seed-a")), ethers.keccak256(ethers.toUtf8Bytes("pqc-order-falcon-seed-b")), ]) ).slice(0, 48); const kp = falcon512.keygen(seed); const pubKey = ethers.hexlify(kp.publicKey); expect(kp.publicKey.length).to.equal(897); const inputAmount = 2500n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount * 2n); const { tuple } = buildOrder(ctx, { user: user.address, nonce: 0, algorithmId: 1, inputAmount, recipient: recipient.address }); const digest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); // Produce a genuine Falcon-512 signature over the 32-byte order digest and confirm // the FULL registry -> adapter -> verifier path accepts it (Falcon signing is // randomized; re-sign on the rare estimation miss, as a real client would). let envelope; for (let attempt = 0; attempt < 6; attempt++) { envelope = realEnvelope(falcon512.sign(ethers.getBytes(digest), kp.secretKey)); if (await ctx.reg.verify(1, pubKey, digest, envelope)) break; envelope = null; } expect(envelope, "could not produce a verifying Falcon-512 leg").to.not.equal(null); const feeSolver = inputAmount - (inputAmount * 50n) / 10_000n; await (await ctx.pool.connect(relayer).openForPQC(tuple, pubKey, envelope, HEAVY)).wait(); expect(await ctx.pool.totalLocked(ctx.inputAddr)).to.equal(feeSolver); expect(await ctx.pool.pqcNonce(user.address)).to.equal(1n); // Tamper one byte deep in the compressed signature -> the real verifier rejects it. const tampered = ethers.getBytes(envelope); tampered[80] ^= 0x01; const next = buildOrder(ctx, { user: user.address, nonce: 1, algorithmId: 1, inputAmount, recipient: recipient.address }); await expect(ctx.pool.connect(relayer).openForPQC(next.tuple, pubKey, ethers.hexlify(tampered), HEAVY)) .to.be.revertedWithCustomError(ctx.pool, "InvalidSignature"); expect(await ctx.pool.pqcNonce(user.address)).to.equal(1n); }); it("SDK order-builder digest matches on-chain byte-for-byte AND an SDK-signed order fills", async function () { await installRealFalcon(); const [_, user, relayer, recipient] = signers; const ctx = await deployStack(1); ctx._now = await latestTs(); // Load the built SDK order-builder (ESM dist). const sdkPath = path.resolve(__dirname, "../../sdk-js/dist/intents/index.js"); const sdk = await import(pathToFileURL(sdkPath).href); // Falcon-512 keypair via the SDK. const seed = ethers.getBytes( ethers.concat([ ethers.keccak256(ethers.toUtf8Bytes("sdk-order-seed-a")), ethers.keccak256(ethers.toUtf8Bytes("sdk-order-seed-b")), ]) ).slice(0, 48); const kp = sdk.generateOrderKey(sdk.PQC_SCHEME.FALCON512, seed); const pubKey = ethers.hexlify(kp.publicKey); const inputAmount = 1234n * 10n ** 6n; await prepUser(ctx, user, pubKey, inputAmount * 2n); const now = await latestTs(); const signed = sdk.buildAndSignPQCOrder({ settler: ctx.poolAddr, chainId, scheme: sdk.PQC_SCHEME.FALCON512, publicKey: kp.publicKey, secretKey: kp.secretKey, params: { user: user.address, nonce: 0, originChainId: chainId, openDeadline: now + 1800, fillDeadline: now + 3600, algorithmId: 1, order: { inputToken: ctx.inputAddr, inputAmount, outputToken: ctx.waereAddr, outputAmount: inputAmount, recipient: recipient.address, destinationChainId: DEST_CHAIN, }, }, }); // Struct field order for the ethers call. const o = signed.order; const tuple = [o.user, o.nonce, o.originChainId, o.openDeadline, o.fillDeadline, o.algorithmId, o.orderDataType, o.orderData]; // (a) The SDK-computed digest equals the on-chain authorizer digest, byte-for-byte. const onchainDigest = await ctx.auth.orderDigest(ctx.poolAddr, tuple, pubKey); expect(signed.digest).to.equal(onchainDigest); // (b) The SDK-built order + genuine Falcon signature fills through the real verifier. await expect(ctx.pool.connect(relayer).openForPQC(tuple, signed.pubKey, signed.signature, HEAVY)) .to.emit(ctx.pool, "PQCOrderOpened"); const solverPortion = inputAmount - (inputAmount * 50n) / 10_000n; expect(await ctx.pool.totalLocked(ctx.inputAddr)).to.equal(solverPortion); expect(await ctx.pool.pqcNonce(user.address)).to.equal(1n); }); }); });