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.
592 lines
29 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|