aere-contracts/test/pqc-intents.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

592 lines
29 KiB
JavaScript

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