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.
99 lines
5.0 KiB
JavaScript
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.`);
|
|
});
|
|
});
|