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

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