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.
214 lines
11 KiB
JavaScript
214 lines
11 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
// Unit tests for the post-quantum ERC-4337 smart account (roadmap #270).
|
|
//
|
|
// The Falcon-512 verification itself is exhaustively KAT-validated in
|
|
// AereFalcon512Verifier.test.js, and the REAL end-to-end Falcon path (genuine
|
|
// reference-generated keypair + signature, verified by the LIVE on-chain
|
|
// verifier) is proven on mainnet by scripts/deploy-pqc-account.js. Here we
|
|
// isolate the ACCOUNT + FACTORY logic with a settable MockFalcon512Verifier so
|
|
// both the accept and reject branches of every path are asserted precisely.
|
|
|
|
const PK = "0x09" + "ab".repeat(896); // 897-byte, 0x09-headed Falcon-512 pubkey shape
|
|
const BAD_PK_LEN = "0x09" + "ab".repeat(10);
|
|
const BAD_PK_HDR = "0x08" + "ab".repeat(896);
|
|
const NONCE = "0x" + "11".repeat(40); // 40-byte Falcon salt
|
|
const COMPSIG = "0x" + "22".repeat(80); // compressed s2 encoding (dummy)
|
|
|
|
const coder = ethers.AbiCoder.defaultAbiCoder();
|
|
const envelope = (nonce, compSig) => coder.encode(["bytes", "bytes"], [nonce, compSig]);
|
|
// EIP-1271 (isValidSignature) verifies the raw 32 bytes of the hash.
|
|
const msgOf = (hash) => hash;
|
|
// The fund-moving paths (validateUserOp, executeWithFalcon) are DOMAIN-SEPARATED:
|
|
// they verify over abi.encodePacked(domain, hash) = 64 bytes. Constants must match
|
|
// the contract, so a raw-hash EIP-1271 login signature can never authorize a spend.
|
|
const USEROP_DOMAIN = ethers.id("AerePQCAccount.validateUserOp.v1");
|
|
const EXEC_DOMAIN = ethers.id("AerePQCAccount.executeWithFalcon.v1");
|
|
const msgDomained = (domain, hash) => ethers.concat([domain, hash]);
|
|
|
|
async function deployFixture() {
|
|
const [deployer, entryPointEOA, other] = await ethers.getSigners();
|
|
const Mock = await ethers.getContractFactory("MockFalcon512Verifier");
|
|
const mock = await Mock.deploy();
|
|
await mock.waitForDeployment();
|
|
|
|
const Factory = await ethers.getContractFactory("AerePQCAccountFactory");
|
|
const factory = await Factory.deploy(entryPointEOA.address, await mock.getAddress());
|
|
await factory.waitForDeployment();
|
|
|
|
return { deployer, entryPointEOA, other, mock, factory };
|
|
}
|
|
|
|
describe("AerePQCAccountFactory", function () {
|
|
it("predicts deterministically and deploys at the predicted address", async function () {
|
|
const { factory } = await deployFixture();
|
|
const predicted = await factory.predictAddress(PK, 0);
|
|
await (await factory.createAccount(PK, 0)).wait();
|
|
expect(await ethers.provider.getCode(predicted)).to.not.equal("0x");
|
|
});
|
|
|
|
it("is idempotent: a second createAccount returns the same account", async function () {
|
|
const { factory } = await deployFixture();
|
|
const predicted = await factory.predictAddress(PK, 0);
|
|
await (await factory.createAccount(PK, 0)).wait();
|
|
await (await factory.createAccount(PK, 0)).wait(); // no revert, no redeploy
|
|
const acct = await ethers.getContractAt("AerePQCAccount", predicted);
|
|
expect((await acct.falconPubKey()).toLowerCase()).to.equal(PK.toLowerCase());
|
|
});
|
|
|
|
it("different salt or different pubkey => different address", async function () {
|
|
const { factory } = await deployFixture();
|
|
const a = await factory.predictAddress(PK, 0);
|
|
const b = await factory.predictAddress(PK, 1);
|
|
const c = await factory.predictAddress("0x09" + "cd".repeat(896), 0);
|
|
expect(a).to.not.equal(b);
|
|
expect(a).to.not.equal(c);
|
|
});
|
|
|
|
it("rejects a malformed Falcon public key", async function () {
|
|
const { factory } = await deployFixture();
|
|
await expect(factory.createAccount(BAD_PK_LEN, 0)).to.be.revertedWithCustomError(factory, "InvalidFalconPubKey");
|
|
await expect(factory.createAccount(BAD_PK_HDR, 0)).to.be.revertedWithCustomError(factory, "InvalidFalconPubKey");
|
|
});
|
|
|
|
it("wires the account's owner key, entryPoint and verifier", async function () {
|
|
const { factory, entryPointEOA, mock } = await deployFixture();
|
|
const predicted = await factory.predictAddress(PK, 7);
|
|
await (await factory.createAccount(PK, 7)).wait();
|
|
const acct = await ethers.getContractAt("AerePQCAccount", predicted);
|
|
expect(await acct.entryPoint()).to.equal(entryPointEOA.address);
|
|
expect(await acct.falcon()).to.equal(await mock.getAddress());
|
|
expect(await acct.factory()).to.equal(await factory.getAddress());
|
|
});
|
|
});
|
|
|
|
describe("AerePQCAccount", function () {
|
|
async function accountFixture() {
|
|
const f = await deployFixture();
|
|
const predicted = await f.factory.predictAddress(PK, 0);
|
|
await (await f.factory.createAccount(PK, 0)).wait();
|
|
const account = await ethers.getContractAt("AerePQCAccount", predicted);
|
|
// Fund the account so it can move value in execute tests.
|
|
await (await f.deployer.sendTransaction({ to: predicted, value: ethers.parseEther("1") })).wait();
|
|
return { ...f, account, predicted };
|
|
}
|
|
|
|
it("initialize cannot be called twice, and only by the factory", async function () {
|
|
const { account, entryPointEOA, mock, deployer } = await accountFixture();
|
|
await expect(
|
|
account.connect(deployer).initialize(entryPointEOA.address, await mock.getAddress(), PK)
|
|
).to.be.revertedWithCustomError(account, "Unauthorized");
|
|
});
|
|
|
|
it("EIP-1271 returns the magic value for a valid Falcon signature", async function () {
|
|
const { account, mock } = await accountFixture();
|
|
const hash = ethers.id("post-quantum login to dapp.example");
|
|
await (await mock.setResult(PK, msgOf(hash), NONCE, COMPSIG, true)).wait();
|
|
expect(await account.isValidSignature(hash, envelope(NONCE, COMPSIG))).to.equal("0x1626ba7e");
|
|
});
|
|
|
|
it("EIP-1271 returns failure (0x00000000) for a tampered signature", async function () {
|
|
const { account, mock } = await accountFixture();
|
|
const hash = ethers.id("post-quantum login to dapp.example");
|
|
await (await mock.setResult(PK, msgOf(hash), NONCE, COMPSIG, true)).wait();
|
|
// Flip one byte of the compressed signature -> the verifier rejects it.
|
|
const tampered = "0x" + "22".repeat(79) + "23";
|
|
expect(await account.isValidSignature(hash, envelope(NONCE, tampered))).to.equal("0x00000000");
|
|
});
|
|
|
|
it("validateUserOp returns 0 for a valid signature (only from the EntryPoint)", async function () {
|
|
const { account, entryPointEOA, mock } = await accountFixture();
|
|
const userOpHash = ethers.id("userop-1");
|
|
await (await mock.setResult(PK, msgDomained(USEROP_DOMAIN, userOpHash), NONCE, COMPSIG, true)).wait();
|
|
const op = [account.target, 0, "0x", "0x", ethers.ZeroHash, 0, ethers.ZeroHash, "0x", envelope(NONCE, COMPSIG)];
|
|
const val = await account.connect(entryPointEOA).validateUserOp.staticCall(op, userOpHash, 0);
|
|
expect(val).to.equal(0n);
|
|
});
|
|
|
|
it("validateUserOp returns 1 (SIG_VALIDATION_FAILED) for a bad signature", async function () {
|
|
const { account, entryPointEOA, mock } = await accountFixture();
|
|
const userOpHash = ethers.id("userop-2");
|
|
await (await mock.setResult(PK, msgDomained(USEROP_DOMAIN, userOpHash), NONCE, COMPSIG, false)).wait();
|
|
const op = [account.target, 0, "0x", "0x", ethers.ZeroHash, 0, ethers.ZeroHash, "0x", envelope(NONCE, COMPSIG)];
|
|
const val = await account.connect(entryPointEOA).validateUserOp.staticCall(op, userOpHash, 0);
|
|
expect(val).to.equal(1n);
|
|
});
|
|
|
|
it("validateUserOp reverts if not called by the EntryPoint", async function () {
|
|
const { account, other } = await accountFixture();
|
|
const op = [account.target, 0, "0x", "0x", ethers.ZeroHash, 0, ethers.ZeroHash, "0x", envelope(NONCE, COMPSIG)];
|
|
await expect(
|
|
account.connect(other).validateUserOp(op, ethers.id("x"), 0)
|
|
).to.be.revertedWithCustomError(account, "Unauthorized");
|
|
});
|
|
|
|
it("executeWithFalcon moves value when the Falcon signature is valid, and bumps the nonce", async function () {
|
|
const { account, mock, other } = await accountFixture();
|
|
const target = other.address;
|
|
const value = ethers.parseEther("0.25");
|
|
const data = "0x";
|
|
const n = await account.execNonce();
|
|
const challenge = await account.getExecuteChallenge(target, value, data, n);
|
|
await (await mock.setResult(PK, msgDomained(EXEC_DOMAIN, challenge), NONCE, COMPSIG, true)).wait();
|
|
|
|
const before = await ethers.provider.getBalance(target);
|
|
await (await account.executeWithFalcon(envelope(NONCE, COMPSIG), target, value, data)).wait();
|
|
const after = await ethers.provider.getBalance(target);
|
|
expect(after - before).to.equal(value);
|
|
expect(await account.execNonce()).to.equal(n + 1n);
|
|
});
|
|
|
|
it("executeWithFalcon reverts on an invalid Falcon signature", async function () {
|
|
const { account, mock, other } = await accountFixture();
|
|
const target = other.address;
|
|
const value = ethers.parseEther("0.1");
|
|
const n = await account.execNonce();
|
|
const challenge = await account.getExecuteChallenge(target, value, "0x", n);
|
|
await (await mock.setResult(PK, msgDomained(EXEC_DOMAIN, challenge), NONCE, COMPSIG, false)).wait();
|
|
await expect(
|
|
account.executeWithFalcon(envelope(NONCE, COMPSIG), target, value, "0x")
|
|
).to.be.revertedWithCustomError(account, "InvalidSignature");
|
|
});
|
|
|
|
// DOMAIN SEPARATION (adversarial-review HIGH, 2026-07-12): a Falcon signature
|
|
// phished through the raw-hash EIP-1271 login surface must NOT authorize a spend.
|
|
it("domain separation: an EIP-1271 login signature cannot be replayed as an executeWithFalcon spend", async function () {
|
|
const { account, mock, other } = await accountFixture();
|
|
const target = other.address;
|
|
const value = ethers.parseEther("0.5");
|
|
const data = "0x";
|
|
const n = await account.execNonce();
|
|
// Attacker computes the exact execute challenge (all inputs are public; execNonce is public).
|
|
const challenge = await account.getExecuteChallenge(target, value, data, n);
|
|
// The user is phished into signing `challenge` as a raw "login" hash: the mock
|
|
// records a VALID Falcon signature for the RAW 32-byte message (the EIP-1271 surface).
|
|
await (await mock.setResult(PK, msgOf(challenge), NONCE, COMPSIG, true)).wait();
|
|
// That login signature IS accepted by the raw-hash isValidSignature surface...
|
|
expect(await account.isValidSignature(challenge, envelope(NONCE, COMPSIG))).to.equal("0x1626ba7e");
|
|
// ...but it must NOT authorize a spend: executeWithFalcon verifies over
|
|
// (EXEC_DOMAIN || challenge), which the phished raw-hash signature does not cover.
|
|
await expect(
|
|
account.executeWithFalcon(envelope(NONCE, COMPSIG), target, value, data)
|
|
).to.be.revertedWithCustomError(account, "InvalidSignature");
|
|
// No value moved, nonce unchanged.
|
|
expect(await account.execNonce()).to.equal(n);
|
|
});
|
|
|
|
it("executeFromEntryPoint is EntryPoint-gated", async function () {
|
|
const { account, other } = await accountFixture();
|
|
await expect(
|
|
account.connect(other).executeFromEntryPoint(other.address, 0, "0x")
|
|
).to.be.revertedWithCustomError(account, "Unauthorized");
|
|
});
|
|
|
|
it("executeFromEntryPoint dispatches the call when invoked by the EntryPoint", async function () {
|
|
const { account, entryPointEOA, other } = await accountFixture();
|
|
const value = ethers.parseEther("0.3");
|
|
const before = await ethers.provider.getBalance(other.address);
|
|
await (await account.connect(entryPointEOA).executeFromEntryPoint(other.address, value, "0x")).wait();
|
|
const after = await ethers.provider.getBalance(other.address);
|
|
expect(after - before).to.equal(value);
|
|
});
|
|
});
|