// AerePQCTxAccount — a post-quantum-authorized ERC-4337 v0.7 smart account (deploy-today reference // path for AIP-PQ-TX). The account's sole owner is a NIST PQC public key, and every fund-moving // authorization (validateUserOp / executeWithPQC) is decided by the LIVE native PQC precompiles on // chain 2800: Falcon-512 0x0AE1, Falcon-1024 0x0AE2, ML-DSA-44 0x0AE3, SLH-DSA-128s 0x0AE4. There is // no ECDSA fallback; a userOp may be relayed by any bundler/EOA and only the PQC key holder can // authorize it. // // Two verification backends are exercised at 0x0AE1..0x0AE4 (the exact pattern used by // AerePQCSocialRecoveryModule.test.js and AereThresholdAccount.test.js): // 1. MockPQCPrecompile — the repo's faithful mock (accept iff commitment == keccak256(pk||msg)), // parsing the input at the precompiles' SPEC offsets, used for the account LOGIC across all four // schemes (accept + reject branches, domain separation, nonce/replay). // 2. RealFalcon512PrecompileAdapter — routes the precompile staticcall into the NIST-KAT-proven // AereFalcon512Verifier, so a subset of tests drive GENUINE Falcon-512 signatures (produced by // @noble/post-quantum, byte-compatible with AERE's live precompile) through validateUserOp. // // Run: npx hardhat test test/AerePQCTxAccount.test.js const { expect } = require("chai"); const { ethers, network } = require("hardhat"); const path = require("path"); const { pathToFileURL } = require("url"); const PRECOMPILE = { 1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), 2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"), 3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), 4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"), }; const META = { 1: { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, falcon: true, name: "Falcon-512" }, 2: { pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, falcon: true, name: "Falcon-1024" }, 3: { pkLen: 1312, sigLen: 2420, falcon: false, name: "ML-DSA-44" }, 4: { pkLen: 32, sigLen: 7856, falcon: false, name: "SLH-DSA-128s" }, }; const ZERO = ethers.ZeroHash; // Domain separators — MUST match AerePQCTxAccount exactly. const USEROP_DOMAIN = ethers.id("AerePQCTxAccount.validateUserOp.v1"); const EXEC_DOMAIN = ethers.id("AerePQCTxAccount.executeWithPQC.v1"); // Fund-moving paths verify over abi.encodePacked(domain, hash) = 64 bytes; EIP-1271 over raw 32 bytes. const domained = (domain, hash) => ethers.concat([domain, hash]); // ---- deterministic seeded RNG (keccak hash-chain) -------------------------- function makeRng(seedText) { let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText)); return { 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; }, }; } 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); } function commitmentFor(pubKey, message) { return ethers.keccak256(ethers.concat([pubKey, message])); } // A genuine MOCK envelope binding to `message` under `pubKey` (the mock at 0x0AE1..4 accepts it). // Envelope shape matches the LIVE precompiles: Falcon = nonce(40)||esig, fixed = raw sig. function genuineMock(scheme, pubKey, message, rng) { const m = META[scheme]; const commitment = commitmentFor(pubKey, message); if (m.falcon) { const nonce = rng.bytes(40); const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]); return ethers.hexlify(ethers.concat([nonce, esig])); } const sig = new Uint8Array(m.sigLen); sig.set(ethers.getBytes(commitment), 0); return ethers.hexlify(sig); } // Flip one byte in the commitment region -> a forgery (the mock rejects it). function tamperMock(scheme, sigHex) { const b = ethers.getBytes(sigHex); b[META[scheme].falcon ? 41 : 0] ^= 0x01; return ethers.hexlify(b); } // PackedUserOperation (v0.7) tuple with `sig` in the signature slot; only `signature` matters here. function userOp(sender, sig) { return [sender, 0, "0x", "0x", ZERO, 0, ZERO, "0x", sig]; } describe("AerePQCTxAccount (post-quantum-authorized ERC-4337 account)", function () { let deployer, ep, other, relayer, stranger; let mockCode; before(async function () { [deployer, ep, other, relayer, stranger] = await ethers.getSigners(); mockCode = {}; const F = await ethers.getContractFactory("MockPQCPrecompile"); for (const s of [1, 2, 3, 4]) { const m = await F.deploy(s); await m.waitForDeployment(); mockCode[s] = await ethers.provider.getCode(await m.getAddress()); } }); async function installMocks() { for (const s of [1, 2, 3, 4]) { await network.provider.send("hardhat_setCode", [PRECOMPILE[s], mockCode[s]]); } } async function deployAccount(scheme, rng, fund = true) { await installMocks(); const pk = makePubKey(scheme, rng); const F = await ethers.getContractFactory("AerePQCTxAccount"); const account = await F.deploy(ep.address, scheme, pk); await account.waitForDeployment(); if (fund) { await (await deployer.sendTransaction({ to: await account.getAddress(), value: ethers.parseEther("1") })).wait(); } return { account, pk, F }; } // ========================================================================= // Construction / config // ========================================================================= describe("construction", function () { it("wires entryPoint, scheme, pubkey and the right precompile for each scheme", async function () { const rng = makeRng("ctor-wire"); for (const scheme of [1, 2, 3, 4]) { const { account, pk } = await deployAccount(scheme, rng, false); expect(await account.entryPoint()).to.equal(ep.address); expect(await account.pqcScheme()).to.equal(scheme); expect((await account.pqcPubKey()).toLowerCase()).to.equal(pk.toLowerCase()); expect(await account.precompileAddress()).to.equal(PRECOMPILE[scheme]); expect(await account.execNonce()).to.equal(0n); } }); it("rejects a zero EntryPoint", async function () { const rng = makeRng("ctor-zeroep"); const F = await ethers.getContractFactory("AerePQCTxAccount"); await expect(F.deploy(ethers.ZeroAddress, 1, makePubKey(1, rng))) .to.be.revertedWithCustomError(F, "InvalidEntryPoint"); }); it("rejects a malformed public key (wrong length or wrong Falcon header)", async function () { const rng = makeRng("ctor-badpk"); const F = await ethers.getContractFactory("AerePQCTxAccount"); // Falcon-512 wrong length await expect(F.deploy(ep.address, 1, "0x09" + "ab".repeat(10))) .to.be.revertedWithCustomError(F, "InvalidPqcPubKey"); // Falcon-512 wrong header (0x08) await expect(F.deploy(ep.address, 1, "0x08" + "ab".repeat(896))) .to.be.revertedWithCustomError(F, "InvalidPqcPubKey"); // ML-DSA-44 wrong length await expect(F.deploy(ep.address, 3, "0x" + "ab".repeat(100))) .to.be.revertedWithCustomError(F, "InvalidPqcPubKey"); }); it("rejects an unknown scheme", async function () { const F = await ethers.getContractFactory("AerePQCTxAccount"); await expect(F.deploy(ep.address, 5, "0x" + "ab".repeat(32))) .to.be.revertedWithCustomError(F, "InvalidScheme"); }); }); // ========================================================================= // validateUserOp — mock verifier, ALL FOUR schemes (accept + reject) // ========================================================================= describe("validateUserOp (mock verifier, all schemes)", function () { for (const scheme of [1, 2, 3, 4]) { it(`ACCEPT/REJECT for ${META[scheme].name} (scheme ${scheme}) via 0x0AE${scheme}`, async function () { const rng = makeRng(`vuo-${scheme}`); const { account, pk } = await deployAccount(scheme, rng, false); const userOpHash = ethers.id(`userop-${scheme}`); const message = domained(USEROP_DOMAIN, userOpHash); // Valid PQC signature over USEROP_DOMAIN||userOpHash -> validationData 0. const good = genuineMock(scheme, pk, message, rng); const okVal = await account .connect(ep) .validateUserOp.staticCall(userOp(await account.getAddress(), good), userOpHash, 0); expect(okVal).to.equal(0n); // Tampered signature -> SIG_VALIDATION_FAILED (1), NOT a revert. const bad = tamperMock(scheme, good); const badVal = await account .connect(ep) .validateUserOp.staticCall(userOp(await account.getAddress(), bad), userOpHash, 0); expect(badVal).to.equal(1n); }); } it("REJECT: a signature over a DIFFERENT userOpHash does not validate (binds the op)", async function () { const rng = makeRng("vuo-wronghash"); const { account, pk } = await deployAccount(1, rng, false); const signed = genuineMock(1, pk, domained(USEROP_DOMAIN, ethers.id("op-A")), rng); // Submit it against op-B's hash. const val = await account .connect(ep) .validateUserOp.staticCall(userOp(await account.getAddress(), signed), ethers.id("op-B"), 0); expect(val).to.equal(1n); }); it("validateUserOp reverts if not called by the EntryPoint", async function () { const rng = makeRng("vuo-auth"); const { account, pk } = await deployAccount(1, rng, false); const h = ethers.id("op"); const sig = genuineMock(1, pk, domained(USEROP_DOMAIN, h), rng); await expect( account.connect(other).validateUserOp(userOp(await account.getAddress(), sig), h, 0) ).to.be.revertedWithCustomError(account, "Unauthorized"); }); it("validateUserOp forwards missingAccountFunds to the EntryPoint as prefund", async function () { const rng = makeRng("vuo-prefund"); const { account, pk } = await deployAccount(1, rng, true); // account funded with 1 ETH const h = ethers.id("op-prefund"); const sig = genuineMock(1, pk, domained(USEROP_DOMAIN, h), rng); const prefund = ethers.parseEther("0.2"); const before = await ethers.provider.getBalance(ep.address); // Real (state-changing) call from the EntryPoint: returns 0 AND sends the prefund. const rc = await ( await account.connect(ep).validateUserOp(userOp(await account.getAddress(), sig), h, prefund) ).wait(); const after = await ethers.provider.getBalance(ep.address); const gasCost = rc.gasUsed * rc.gasPrice; // ep received `prefund` minus the gas it paid to send the tx. expect(after - before + gasCost).to.equal(prefund); }); }); // ========================================================================= // executeWithPQC (self-relay) + EIP-1271 + domain separation // ========================================================================= describe("executeWithPQC / EIP-1271 / domain separation (mock verifier)", function () { it("executeWithPQC moves value on a valid signature and bumps execNonce", async function () { const rng = makeRng("exec-happy"); const { account, pk } = await deployAccount(1, rng, true); 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); const sig = genuineMock(1, pk, domained(EXEC_DOMAIN, challenge), rng); const before = await ethers.provider.getBalance(target); await (await account.connect(relayer).executeWithPQC(sig, target, value, data)).wait(); expect((await ethers.provider.getBalance(target)) - before).to.equal(value); expect(await account.execNonce()).to.equal(n + 1n); }); it("executeWithPQC reverts on an invalid signature; no value moves, nonce unchanged", async function () { const rng = makeRng("exec-bad"); const { account, pk } = await deployAccount(3, rng, true); // ML-DSA-44 owner const target = other.address; const value = ethers.parseEther("0.1"); const n = await account.execNonce(); const challenge = await account.getExecuteChallenge(target, value, "0x", n); const bad = tamperMock(3, genuineMock(3, pk, domained(EXEC_DOMAIN, challenge), rng)); await expect(account.executeWithPQC(bad, target, value, "0x")) .to.be.revertedWithCustomError(account, "InvalidSignature"); expect(await account.execNonce()).to.equal(n); }); it("executeWithPQC legs are single-use: replaying the same signature fails (nonce advanced)", async function () { const rng = makeRng("exec-replay"); const { account, pk } = await deployAccount(1, rng, true); const target = other.address; const value = ethers.parseEther("0.05"); const n = await account.execNonce(); const challenge = await account.getExecuteChallenge(target, value, "0x", n); const sig = genuineMock(1, pk, domained(EXEC_DOMAIN, challenge), rng); await (await account.executeWithPQC(sig, target, value, "0x")).wait(); // execNonce is now n+1, so the same signature (bound to nonce n) no longer authorizes. await expect(account.executeWithPQC(sig, target, value, "0x")) .to.be.revertedWithCustomError(account, "InvalidSignature"); }); it("EIP-1271 returns the magic value for a valid raw-hash signature, 0 for a tampered one", async function () { const rng = makeRng("1271"); const { account, pk } = await deployAccount(1, rng, false); const hash = ethers.id("post-quantum login to dapp.example"); const sig = genuineMock(1, pk, hash, rng); // raw 32-byte message expect(await account.isValidSignature(hash, sig)).to.equal("0x1626ba7e"); expect(await account.isValidSignature(hash, tamperMock(1, sig))).to.equal("0x00000000"); }); it("DOMAIN SEPARATION: an EIP-1271 login signature cannot be replayed as an executeWithPQC spend", async function () { const rng = makeRng("domain-sep"); const { account, pk } = await deployAccount(1, rng, true); 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 public; execNonce is public). const challenge = await account.getExecuteChallenge(target, value, data, n); // User is phished into signing `challenge` as a RAW login hash (the EIP-1271 surface). const loginSig = genuineMock(1, pk, challenge, rng); // commitment over the RAW 32-byte challenge // That login signature IS accepted by the raw-hash isValidSignature surface... expect(await account.isValidSignature(challenge, loginSig)).to.equal("0x1626ba7e"); // ...but must NOT authorize a spend: executeWithPQC verifies over (EXEC_DOMAIN || challenge) = 64 bytes. await expect(account.executeWithPQC(loginSig, target, value, data)) .to.be.revertedWithCustomError(account, "InvalidSignature"); expect(await account.execNonce()).to.equal(n); // nothing moved }); }); // ========================================================================= // EntryPoint-gated dispatch // ========================================================================= describe("executeFromEntryPoint / executeBatchFromEntryPoint", function () { it("executeFromEntryPoint is EntryPoint-gated and dispatches when the EntryPoint calls", async function () { const rng = makeRng("efe"); const { account } = await deployAccount(1, rng, true); await expect(account.connect(other).executeFromEntryPoint(other.address, 0, "0x")) .to.be.revertedWithCustomError(account, "Unauthorized"); const value = ethers.parseEther("0.3"); const before = await ethers.provider.getBalance(other.address); await (await account.connect(ep).executeFromEntryPoint(other.address, value, "0x")).wait(); expect((await ethers.provider.getBalance(other.address)) - before).to.equal(value); }); it("executeBatchFromEntryPoint dispatches every call and is EntryPoint-gated", async function () { const rng = makeRng("ebe"); const { account } = await deployAccount(1, rng, true); const calls = [ [stranger.address, ethers.parseEther("0.1"), "0x"], [other.address, ethers.parseEther("0.2"), "0x"], ]; await expect(account.connect(other).executeBatchFromEntryPoint(calls)) .to.be.revertedWithCustomError(account, "Unauthorized"); const b1 = await ethers.provider.getBalance(stranger.address); const b2 = await ethers.provider.getBalance(other.address); await (await account.connect(ep).executeBatchFromEntryPoint(calls)).wait(); expect((await ethers.provider.getBalance(stranger.address)) - b1).to.equal(ethers.parseEther("0.1")); expect((await ethers.provider.getBalance(other.address)) - b2).to.equal(ethers.parseEther("0.2")); }); }); // ========================================================================= // REAL Falcon-512 crypto: genuine signatures via @noble, verified by the // NIST-KAT-proven AereFalcon512Verifier behind a precompile-format adapter. // ========================================================================= describe("real Falcon-512 authorization (KAT-proven verifier, no mock)", function () { let falcon512, adapterCode; // Pure-Solidity Falcon-512 verify (stand-in for the native precompile) is ~10.7M gas; give the // heavy nested-staticcall paths ample headroom (the hardhat network permits a large block gas // limit). TEST-HARNESS accommodation for the expensive stand-in, not a contract concern. const HEAVY = { gasLimit: 400_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)); const V = await ethers.getContractFactory("AereFalcon512Verifier"); const verifier = await V.deploy(); await verifier.waitForDeployment(); const A = await ethers.getContractFactory("RealFalcon512PrecompileAdapter"); const adapter = await A.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 installRealFalconAt0AE1() { await network.provider.send("hardhat_setCode", [PRECOMPILE[1], adapterCode]); } async function deployRealAccount(seedFill) { await installRealFalconAt0AE1(); const kp = falcon512.keygen(new Uint8Array(48).fill(seedFill)); const pk = ethers.hexlify(kp.publicKey); const F = await ethers.getContractFactory("AerePQCTxAccount"); const account = await F.deploy(ep.address, 1, pk); await account.waitForDeployment(); return { account, kp }; } it("validateUserOp returns 0 for a GENUINE Falcon-512 signature over the domained userOpHash", async function () { const { account, kp } = await deployRealAccount(0x11); const userOpHash = ethers.id("real-userop-accept"); const message = ethers.getBytes(domained(USEROP_DOMAIN, userOpHash)); // 64 bytes // Falcon signing is randomized; re-sign until the full account->adapter->verifier path accepts. let accepted = false; for (let attempt = 0; attempt < 8 && !accepted; attempt++) { const env = realEnvelope(falcon512.sign(message, kp.secretKey)); const val = await account .connect(ep) .validateUserOp.staticCall(userOp(await account.getAddress(), env), userOpHash, 0, HEAVY); if (val === 0n) accepted = true; } expect(accepted, "a genuine Falcon-512 signature should validate").to.equal(true); }); it("validateUserOp returns 1 (SIG_VALIDATION_FAILED) for a tampered Falcon-512 signature", async function () { const { account, kp } = await deployRealAccount(0x22); const userOpHash = ethers.id("real-userop-reject"); const message = ethers.getBytes(domained(USEROP_DOMAIN, userOpHash)); const env = ethers.getBytes(realEnvelope(falcon512.sign(message, kp.secretKey))); env[50] ^= 0x01; // flip a byte inside the compressed signature body -> forgery const val = await account .connect(ep) .validateUserOp.staticCall(userOp(await account.getAddress(), ethers.hexlify(env)), userOpHash, 0, HEAVY); expect(val).to.equal(1n); }); }); });