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

99 lines
5.0 KiB
JavaScript

// Property-based fuzz of AereThresholdAccount.validateUserOp: on top of the halmos proofs and the
// unit tests, hammer the counting logic with hundreds of random leg combinations (genuine / forged
// / junk / duplicate / out-of-range) over fresh challenges, and assert the account authorizes iff
// the number of DISTINCT in-range members with a genuine signature is >= threshold. Deterministic
// (seeded) so any failure reproduces.
const { expect } = require("chai");
const { ethers, network } = require("hardhat");
const PRECOMPILE = {
1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"),
2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"),
3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"),
4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"),
};
const M = { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29 };
function makeRng(seedText) {
let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
return {
next() { seed = ethers.keccak256(seed); return BigInt(seed); },
int(n) { return Number(this.next() % BigInt(n)); },
bytes(n) {
const out = new Uint8Array(n); let i = 0;
while (i < n) { seed = ethers.keccak256(seed); const c = ethers.getBytes(seed); for (let j = 0; j < c.length && i < n; j++, i++) out[i] = c[j]; }
return out;
},
};
}
function makePubKey(rng) { const raw = rng.bytes(M.pkLen); raw[0] = M.pkHeader; return ethers.hexlify(raw); }
function genuine(pubKey, message, rng) {
const commitment = ethers.keccak256(ethers.concat([pubKey, message]));
return ethers.hexlify(ethers.concat([rng.bytes(40), new Uint8Array([M.esigHeader]), ethers.getBytes(commitment)]));
}
function forged(pubKey, message, rng) { const b = ethers.getBytes(genuine(pubKey, message, rng)); b[41] ^= 0x01; return ethers.hexlify(b); }
function junk(rng) { return ethers.hexlify(rng.bytes(41 + rng.int(60))); }
function encodeLegs(legs) {
return ethers.AbiCoder.defaultAbiCoder().encode(
["tuple(uint8 memberIndex, bytes signature)[]"], [legs.map((l) => [l.idx, l.sig])]
);
}
const SCHEME = 1, N = 6, T = 3, ITERS = 300;
describe("AereThresholdAccount — property-based fuzz (300 random leg combos)", function () {
this.timeout(120000);
let mockCode, account, entryPoint, echo, pubKeys, rng;
before(async function () {
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()); }
for (const s of [1, 2, 3, 4]) await network.provider.send("hardhat_setCode", [PRECOMPILE[s], mockCode[s]]);
[entryPoint] = await ethers.getSigners();
rng = makeRng("fuzz-seed-v1");
pubKeys = []; for (let i = 0; i < N; i++) pubKeys.push(makePubKey(rng));
const Fac = await ethers.getContractFactory("AereThresholdAccountFactory");
const factory = await Fac.deploy(); await factory.waitForDeployment();
const salt = ethers.id("fuzz");
const addr = await factory.computeAddress(entryPoint.address, SCHEME, T, pubKeys, salt);
await (await factory.createAccount(entryPoint.address, SCHEME, T, pubKeys, salt)).wait();
account = await ethers.getContractAt("AereThresholdAccount", addr);
echo = await (await ethers.getContractFactory("EchoTarget")).deploy(); await echo.waitForDeployment();
});
it(`authorizes iff distinct in-range genuine signers >= threshold, across ${ITERS} random inputs`, async function () {
const acctAddr = await account.getAddress();
let checked = 0;
for (let iter = 0; iter < ITERS; iter++) {
const userOpHash = ethers.id("fuzz-op-" + iter);
const challenge = await account.userOpChallenge(userOpHash);
const numLegs = 1 + rng.int(8);
const legs = [];
const distinctValid = new Set();
for (let k = 0; k < numLegs; k++) {
const idx = rng.int(N + 2); // 0..N+1 → indices N,N+1 are out-of-range
const kind = rng.int(3); // 0 genuine, 1 forged, 2 junk
let sig;
if (kind === 0) { sig = genuine(pubKeys[idx % N], challenge, rng); if (idx < N) distinctValid.add(idx); }
else if (kind === 1) sig = forged(pubKeys[idx % N], challenge, rng);
else sig = junk(rng);
legs.push({ idx, sig });
}
const expected = distinctValid.size >= T ? 0n : 1n;
const op = {
sender: acctAddr, nonce: 0n, initCode: "0x", callData: "0x",
accountGasLimits: ethers.ZeroHash, preVerificationGas: 0n, gasFees: ethers.ZeroHash,
paymasterAndData: "0x", signature: encodeLegs(legs),
};
const got = await account.connect(entryPoint).validateUserOp.staticCall(op, userOpHash, 0);
if (got !== expected) {
throw new Error(`FUZZ MISMATCH iter ${iter}: distinct-valid=${distinctValid.size} t=${T} expected=${expected} got=${got} legs=${JSON.stringify(legs.map((l) => l.idx))}`);
}
checked++;
}
expect(checked).to.equal(ITERS);
console.log(`\n fuzz: ${ITERS}/${ITERS} random leg combinations matched the threshold invariant exactly.`);
});
});