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

660 lines
29 KiB
JavaScript

const { expect } = require("chai");
const { ethers, network } = require("hardhat");
// ---------------------------------------------------------------------------
// AereCryptoRegistry — access control, seeding, routed verification, lifecycle
// policy, successor resolution, and seeded invariant fuzz.
//
// Hardhat has no PQC precompile, so we install MockPQCPrecompile at 0x0AE1..0x0AE4
// (hardhat_setCode), exactly as the AerePQCAttestation test does. The mock parses the
// input at the precompiles' SPEC offsets and accepts iff commitment == keccak256(pk ||
// message). The registry builds the SAME wire format, so a positive verify() through the
// registry proves it encoded the input at the correct offsets. The REAL encoding is
// separately proven against the LIVE mainnet precompiles via eth_call (AerePQCAttestation
// suite / scripts). SHAKE256 (scheme 5) is a HASH entry the registry refuses to route.
// ---------------------------------------------------------------------------
const PRECOMPILE = {
1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), // Falcon-512
2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"), // Falcon-1024
3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), // ML-DSA-44
4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"), // SLH-DSA-128s
};
// Scheme metadata identical to the live seeded rows; the seeded algorithmId equals the
// scheme number (id 1 = Falcon-512, ... id 4 = SLH-DSA-128s, id 5 = SHAKE256).
const META = {
1: { name: "Falcon-512", pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, sigLen: 0, falcon: true, gas: 120000 },
2: { name: "Falcon-1024", pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, sigLen: 0, falcon: true, gas: 145000 },
3: { name: "ML-DSA-44", pkLen: 1312, esigHeader: 0x00, sigLen: 2420, falcon: false, gas: 351000 },
4: { name: "SLH-DSA-128s", pkLen: 32, esigHeader: 0x00, sigLen: 7856, falcon: false, gas: 400000 },
};
const Status = { UNKNOWN: 0, ACTIVE: 1, DEPRECATED: 2, REVOKED: 3 };
// Deterministic seeded RNG (keccak hash-chain), so the fuzz is fully reproducible.
function makeRng(seedText) {
let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
function 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 int(n) {
const b = bytes(4);
return (((b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]) >>> 0) % n;
}
return { bytes, int };
}
function makePubKey(scheme, rng) {
const m = META[scheme];
const raw = rng.bytes(m.pkLen);
if (m.falcon) raw[0] = m.pkHeader; // realistic Falcon header (not required by the mock)
return ethers.hexlify(raw);
}
// commitment the mock expects for a genuine signature over `message` by `pubKey`.
function commitmentFor(pubKey, message) {
return ethers.keccak256(ethers.concat([pubKey, message]));
}
// Build the signature envelope with a chosen commitment (genuine or forged).
function envelope(scheme, commitment, rng) {
const m = META[scheme];
if (m.falcon) {
const nonce = rng.bytes(40);
const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]); // 33 bytes
return ethers.hexlify(ethers.concat([nonce, esig]));
} else {
const sig = new Uint8Array(m.sigLen);
sig.set(ethers.getBytes(commitment), 0);
return ethers.hexlify(sig);
}
}
// A genuine signature envelope binding to `message` (a bytes32) under `pubKey`.
function genuine(scheme, pubKey, message, rng) {
return envelope(scheme, commitmentFor(pubKey, message), rng);
}
// Flip one byte inside the commitment region -> a genuine forgery (mock rejects it).
function tamperCommitment(scheme, sigHex) {
const b = ethers.getBytes(sigHex);
const idx = META[scheme].falcon ? 41 : 0; // Falcon: esig[1] at 40+1; fixed: sig[0]
b[idx] ^= 0x01;
return ethers.hexlify(b);
}
describe("AereCryptoRegistry", function () {
let reg, owner, other, mockCode;
before(async function () {
[owner, other] = await ethers.getSigners();
// Deploy one mock per scheme; capture its runtime bytecode (immutable scheme baked in).
mockCode = {};
const F = await ethers.getContractFactory("MockPQCPrecompile");
for (const scheme of [1, 2, 3, 4]) {
const mock = await F.deploy(scheme);
await mock.waitForDeployment();
mockCode[scheme] = await ethers.provider.getCode(await mock.getAddress());
}
});
async function installMocks() {
for (const scheme of [1, 2, 3, 4]) {
await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], mockCode[scheme]]);
}
}
beforeEach(async function () {
await installMocks();
const R = await ethers.getContractFactory("AereCryptoRegistry");
reg = await R.deploy();
await reg.waitForDeployment();
});
// Send addAlgorithm, parse the event, return the new id.
async function addAlgo(args, signer = owner) {
const tx = await reg.connect(signer).addAlgorithm(...args);
const rc = await tx.wait();
const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "AlgorithmAdded");
return Number(log.args.id);
}
// Convenience args for a Falcon-512-style row pointing at the scheme-1 mock.
function falcon512Args(name, gas = 120000) {
return [name, 1, PRECOMPILE[1], 897, 0, 0x29, gas, true];
}
// -----------------------------------------------------------------------
// Access control
// -----------------------------------------------------------------------
describe("access control (Ownable)", function () {
const OWNABLE_REVERT = "Ownable: caller is not the owner";
it("sets the deployer as owner", async function () {
expect(await reg.owner()).to.equal(owner.address);
});
it("reverts for a non-owner on addAlgorithm / setStatus / setSuccessor / seedLiveSchemes", async function () {
await expect(reg.connect(other).addAlgorithm(...falcon512Args("X"))).to.be.revertedWith(OWNABLE_REVERT);
await expect(reg.connect(other).setStatus(1, Status.REVOKED)).to.be.revertedWith(OWNABLE_REVERT);
await expect(reg.connect(other).setSuccessor(1, 2)).to.be.revertedWith(OWNABLE_REVERT);
await expect(reg.connect(other).seedLiveSchemes()).to.be.revertedWith(OWNABLE_REVERT);
});
it("lets the owner add an algorithm", async function () {
const id = await addAlgo(falcon512Args("Falcon-512"));
expect(id).to.equal(1);
expect(await reg.algorithmCount()).to.equal(1n);
});
});
// -----------------------------------------------------------------------
// addAlgorithm validation
// -----------------------------------------------------------------------
describe("addAlgorithm validation", function () {
it("rejects an empty or over-long name", async function () {
await expect(reg.addAlgorithm("", 1, PRECOMPILE[1], 897, 0, 0x29, 1, true)).to.be.revertedWithCustomError(
reg,
"InvalidAlgorithmParams"
);
const long = "n".repeat(65);
await expect(reg.addAlgorithm(long, 1, PRECOMPILE[1], 897, 0, 0x29, 1, true)).to.be.revertedWithCustomError(
reg,
"InvalidAlgorithmParams"
);
});
it("rejects a zero verifier", async function () {
await expect(reg.addAlgorithm("X", 1, ethers.ZeroAddress, 897, 0, 0x29, 1, true)).to.be.revertedWithCustomError(
reg,
"InvalidAlgorithmParams"
);
});
it("rejects a signature scheme with pubKeyLen 0", async function () {
await expect(reg.addAlgorithm("X", 1, PRECOMPILE[1], 0, 0, 0x29, 1, true)).to.be.revertedWithCustomError(
reg,
"InvalidAlgorithmParams"
);
});
it("rejects a fixed-concatenation signature scheme with sigLen 0", async function () {
await expect(reg.addAlgorithm("X", 3, PRECOMPILE[3], 1312, 0, 0x00, 1, true)).to.be.revertedWithCustomError(
reg,
"InvalidAlgorithmParams"
);
});
it("allows a hash-only entry with pubKeyLen 0 and sigLen 0", async function () {
const id = await addAlgo(["SHAKE256", 5, PRECOMPILE[1], 0, 0, 0x00, 30000, false]);
const a = await reg.getAlgorithm(id);
expect(a.isSignature).to.equal(false);
});
});
// -----------------------------------------------------------------------
// Seeding
// -----------------------------------------------------------------------
describe("seedLiveSchemes", function () {
it("registers exactly the five live schemes with correct fields", async function () {
await reg.seedLiveSchemes();
expect(await reg.algorithmCount()).to.equal(5n);
expect(await reg.seeded()).to.equal(true);
const expected = [
{ id: 1, name: "Falcon-512", scheme: 1, verifier: PRECOMPILE[1], pk: 897, sig: 0, esig: 0x29, sign: true, gas: 120000 },
{ id: 2, name: "Falcon-1024", scheme: 2, verifier: PRECOMPILE[2], pk: 1793, sig: 0, esig: 0x2a, sign: true, gas: 145000 },
{ id: 3, name: "ML-DSA-44", scheme: 3, verifier: PRECOMPILE[3], pk: 1312, sig: 2420, esig: 0x00, sign: true, gas: 351000 },
{ id: 4, name: "SLH-DSA-128s", scheme: 4, verifier: PRECOMPILE[4], pk: 32, sig: 7856, esig: 0x00, sign: true, gas: 400000 },
{ id: 5, name: "SHAKE256", scheme: 5, verifier: ethers.getAddress("0x0000000000000000000000000000000000000ae5"), pk: 0, sig: 0, esig: 0x00, sign: false, gas: 30000 },
];
for (const e of expected) {
const a = await reg.getAlgorithm(e.id);
expect(a.name).to.equal(e.name);
expect(Number(a.scheme)).to.equal(e.scheme);
expect(a.verifier).to.equal(e.verifier);
expect(Number(a.pubKeyLen)).to.equal(e.pk);
expect(Number(a.sigLen)).to.equal(e.sig);
expect(Number(a.esigHeader)).to.equal(e.esig);
expect(Number(a.status)).to.equal(Status.ACTIVE);
expect(Number(a.gasEstimate)).to.equal(e.gas);
expect(Number(a.successorId)).to.equal(0);
expect(a.isSignature).to.equal(e.sign);
expect(Number(a.addedBlock)).to.be.greaterThan(0);
}
});
it("is one-shot: a second seed reverts AlreadySeeded", async function () {
await reg.seedLiveSchemes();
await expect(reg.seedLiveSchemes()).to.be.revertedWithCustomError(reg, "AlreadySeeded");
});
it("precompileFor(scheme) returns the ACTIVE verifier and reverts for an unknown scheme", async function () {
await reg.seedLiveSchemes();
expect(await reg.precompileFor(1)).to.equal(PRECOMPILE[1]);
expect(await reg.precompileFor(4)).to.equal(PRECOMPILE[4]);
await expect(reg.precompileFor(9)).to.be.revertedWithCustomError(reg, "UnknownScheme");
});
});
// -----------------------------------------------------------------------
// Routed verification (happy path per scheme)
// -----------------------------------------------------------------------
describe("verify (routed happy path)", function () {
beforeEach(async function () {
await reg.seedLiveSchemes();
});
for (const scheme of [1, 2, 3, 4]) {
it(`routes ${META[scheme].name} and returns true for genuine, false for tampered`, async function () {
const rng = makeRng("verify-" + scheme);
const pk = makePubKey(scheme, rng);
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("msg-" + scheme));
const good = genuine(scheme, pk, messageHash, rng);
expect(await reg.verify(scheme, pk, messageHash, good)).to.equal(true);
const bad = tamperCommitment(scheme, good);
expect(await reg.verify(scheme, pk, messageHash, bad)).to.equal(false);
});
}
it("returns false when the message differs from the signed one", async function () {
const rng = makeRng("wrong-msg");
const scheme = 2;
const pk = makePubKey(scheme, rng);
const signed = ethers.keccak256(ethers.toUtf8Bytes("signed"));
const other = ethers.keccak256(ethers.toUtf8Bytes("other"));
const sig = genuine(scheme, pk, signed, rng);
expect(await reg.verify(scheme, pk, other, sig)).to.equal(false);
});
it("returns false for a signature by a different key", async function () {
const rng = makeRng("wrong-key");
const scheme = 3;
const pk = makePubKey(scheme, rng);
const otherPk = makePubKey(scheme, rng);
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
const sigOther = genuine(scheme, otherPk, messageHash, rng);
expect(await reg.verify(scheme, pk, messageHash, sigOther)).to.equal(false);
});
it("returns false for a wrong-length public key or signature (fail-closed, no revert)", async function () {
const rng = makeRng("bad-len");
const scheme = 3; // ML-DSA-44: pk 1312, sig 2420
const pk = makePubKey(scheme, rng);
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
const good = genuine(scheme, pk, messageHash, rng);
// short pk
expect(await reg.verify(scheme, "0x" + "00".repeat(1311), messageHash, good)).to.equal(false);
// short signature
expect(await reg.verify(scheme, pk, messageHash, "0x" + "00".repeat(2419))).to.equal(false);
// Falcon with only a nonce (no esig)
const f = makePubKey(1, rng);
expect(await reg.verify(1, f, messageHash, "0x" + "00".repeat(40))).to.equal(false);
});
it("returns false for a hash-only entry (SHAKE256, isSignature=false)", async function () {
const rng = makeRng("hash");
const anyPk = makePubKey(1, rng);
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
const anySig = genuine(1, anyPk, messageHash, rng);
expect(await reg.verify(5, anyPk, messageHash, anySig)).to.equal(false);
});
it("returns false for an unknown algorithmId (no revert)", async function () {
const rng = makeRng("unknown");
const pk = makePubKey(1, rng);
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
const sig = genuine(1, pk, messageHash, rng);
expect(await reg.verify(0, pk, messageHash, sig)).to.equal(false);
expect(await reg.verify(999, pk, messageHash, sig)).to.equal(false);
});
it("treats an empty verifier return (pre-fork / wiped) as invalid", async function () {
const rng = makeRng("empty");
const scheme = 2;
const pk = makePubKey(scheme, rng);
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
const sig = genuine(scheme, pk, messageHash, rng);
expect(await reg.verify(scheme, pk, messageHash, sig)).to.equal(true);
await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], "0x"]);
expect(await reg.verify(scheme, pk, messageHash, sig)).to.equal(false);
});
});
// -----------------------------------------------------------------------
// Explicit encoding check: registry input == the precompile SPEC layout.
// -----------------------------------------------------------------------
describe("encoding matches the precompile spec", function () {
beforeEach(async function () {
await reg.seedLiveSchemes();
});
it("Falcon-1024: the same (pk,msg,sig) verified directly by the mock also verifies via the registry", async function () {
const rng = makeRng("enc-falcon");
const scheme = 2;
const m = META[scheme];
const pk = ethers.getBytes(makePubKey(scheme, rng));
const message = ethers.getBytes(ethers.keccak256(ethers.toUtf8Bytes("enc")));
const commitment = ethers.getBytes(commitmentFor(ethers.hexlify(pk), ethers.hexlify(message)));
const nonce = rng.bytes(40);
const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]);
const sigLen = ethers.getBytes(esig).length;
const sm = ethers.concat([new Uint8Array([(sigLen >> 8) & 0xff, sigLen & 0xff]), nonce, message, esig]);
const specInput = ethers.hexlify(ethers.concat([pk, sm]));
// Direct precompile call with the hand-built spec input.
const ret = await ethers.provider.call({ to: PRECOMPILE[scheme], data: specInput });
expect(BigInt(ret)).to.equal(1n);
// The registry, given the envelope (nonce || esig), must build the identical input.
const env = ethers.hexlify(ethers.concat([nonce, esig]));
expect(await reg.verify(scheme, ethers.hexlify(pk), ethers.hexlify(message), env)).to.equal(true);
});
it("ML-DSA-44: fixed concatenation pk||sig||msg matches", async function () {
const rng = makeRng("enc-mldsa");
const scheme = 3;
const m = META[scheme];
const pk = ethers.getBytes(makePubKey(scheme, rng));
const message = ethers.getBytes(ethers.keccak256(ethers.toUtf8Bytes("enc2")));
const commitment = ethers.getBytes(commitmentFor(ethers.hexlify(pk), ethers.hexlify(message)));
const sig = new Uint8Array(m.sigLen);
sig.set(commitment, 0);
const specInput = ethers.hexlify(ethers.concat([pk, sig, message]));
const ret = await ethers.provider.call({ to: PRECOMPILE[scheme], data: specInput });
expect(BigInt(ret)).to.equal(1n);
expect(await reg.verify(scheme, ethers.hexlify(pk), ethers.hexlify(message), ethers.hexlify(sig))).to.equal(true);
});
});
// -----------------------------------------------------------------------
// Lifecycle policy: UNKNOWN / REVOKED rejected; DEPRECATED still verifies
// -----------------------------------------------------------------------
describe("lifecycle policy", function () {
beforeEach(async function () {
await reg.seedLiveSchemes();
});
it("DEPRECATED still verifies, but isActive=false and isUsable=false", async function () {
const rng = makeRng("dep");
const scheme = 1;
const pk = makePubKey(scheme, rng);
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
const sig = genuine(scheme, pk, messageHash, rng);
await reg.setStatus(1, Status.DEPRECATED);
expect(Number(await reg.statusOf(1))).to.equal(Status.DEPRECATED);
expect(await reg.isActive(1)).to.equal(false);
expect(await reg.isUsable(1)).to.equal(false);
expect(await reg.verify(1, pk, messageHash, sig)).to.equal(true); // legacy verify still works
});
it("REVOKED never verifies", async function () {
const rng = makeRng("rev");
const scheme = 1;
const pk = makePubKey(scheme, rng);
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
const sig = genuine(scheme, pk, messageHash, rng);
expect(await reg.verify(1, pk, messageHash, sig)).to.equal(true);
await reg.setStatus(1, Status.REVOKED);
expect(await reg.verify(1, pk, messageHash, sig)).to.equal(false);
expect(await reg.isActive(1)).to.equal(false);
expect(await reg.isUsable(1)).to.equal(false);
});
it("an ACTIVE hash-only entry is isActive but not isUsable", async function () {
expect(await reg.isActive(5)).to.equal(true); // SHAKE256 seeded ACTIVE
expect(await reg.isUsable(5)).to.equal(false); // but not a usable signature verifier
});
it("setStatus cannot set UNKNOWN", async function () {
await expect(reg.setStatus(1, Status.UNKNOWN)).to.be.revertedWithCustomError(reg, "InvalidStatusTarget");
});
it("REVOKED is terminal: no further status change", async function () {
await reg.setStatus(1, Status.REVOKED);
await expect(reg.setStatus(1, Status.ACTIVE)).to.be.revertedWithCustomError(reg, "AlgorithmRevoked");
await expect(reg.setStatus(1, Status.DEPRECATED)).to.be.revertedWithCustomError(reg, "AlgorithmRevoked");
});
it("a DEPRECATED row can be reactivated to ACTIVE", async function () {
await reg.setStatus(1, Status.DEPRECATED);
await reg.setStatus(1, Status.ACTIVE);
expect(await reg.isActive(1)).to.equal(true);
expect(await reg.isUsable(1)).to.equal(true);
});
it("setStatus / setSuccessor revert on an unknown id", async function () {
await expect(reg.setStatus(999, Status.ACTIVE)).to.be.revertedWithCustomError(reg, "UnknownAlgorithm");
await expect(reg.setSuccessor(999, 1)).to.be.revertedWithCustomError(reg, "UnknownAlgorithm");
await expect(reg.setSuccessor(1, 999)).to.be.revertedWithCustomError(reg, "UnknownAlgorithm");
});
it("statusOf/isActive/isUsable report UNKNOWN/false for a never-registered id", async function () {
expect(Number(await reg.statusOf(999))).to.equal(Status.UNKNOWN);
expect(await reg.isActive(999)).to.equal(false);
expect(await reg.isUsable(999)).to.equal(false);
});
});
// -----------------------------------------------------------------------
// Successor resolution
// -----------------------------------------------------------------------
describe("resolveActive (successor chain)", function () {
beforeEach(async function () {
await reg.seedLiveSchemes();
});
it("returns the id itself when it is ACTIVE", async function () {
expect(Number(await reg.resolveActive(1))).to.equal(1);
});
it("follows a single successor from a DEPRECATED row to its ACTIVE successor", async function () {
const v2 = await addAlgo(falcon512Args("Falcon-512-v2")); // id 6, ACTIVE
await reg.setStatus(1, Status.DEPRECATED);
await reg.setSuccessor(1, v2);
expect(Number(await reg.resolveActive(1))).to.equal(v2);
});
it("follows a multi-hop chain to the first ACTIVE row", async function () {
const v2 = await addAlgo(falcon512Args("Falcon-512-v2")); // id 6
const v3 = await addAlgo(falcon512Args("Falcon-512-v3")); // id 7
await reg.setStatus(1, Status.DEPRECATED);
await reg.setStatus(v2, Status.DEPRECATED);
await reg.setSuccessor(1, v2);
await reg.setSuccessor(v2, v3);
expect(Number(await reg.resolveActive(1))).to.equal(v3);
});
it("reverts NoActiveSuccessor when the chain dead-ends with no ACTIVE row", async function () {
await reg.setStatus(2, Status.DEPRECATED); // no successor set
await expect(reg.resolveActive(2)).to.be.revertedWithCustomError(reg, "NoActiveSuccessor");
});
it("reverts SuccessorCycle on a cyclic chain with no ACTIVE row", async function () {
const x = await addAlgo(falcon512Args("X")); // id 6
const y = await addAlgo(falcon512Args("Y")); // id 7
await reg.setStatus(x, Status.DEPRECATED);
await reg.setStatus(y, Status.DEPRECATED);
await reg.setSuccessor(x, y);
await reg.setSuccessor(y, x);
await expect(reg.resolveActive(x)).to.be.revertedWithCustomError(reg, "SuccessorCycle");
});
it("reverts UnknownAlgorithm for a nonexistent id", async function () {
await expect(reg.resolveActive(999)).to.be.revertedWithCustomError(reg, "UnknownAlgorithm");
});
it("setSuccessor rejects a self-successor", async function () {
await expect(reg.setSuccessor(1, 1)).to.be.revertedWithCustomError(reg, "InvalidSuccessor");
});
});
// -----------------------------------------------------------------------
// Seeded invariant fuzz
// -----------------------------------------------------------------------
describe("seeded invariant fuzz", function () {
it("a REVOKED algorithm never verifies, across many rows and random statuses", async function () {
const rng = makeRng("fuzz-revoked-v1");
const N = 16;
const rows = [];
for (let i = 0; i < N; i++) {
const id = await addAlgo(falcon512Args("F-" + i));
const pk = makePubKey(1, rng);
const messageHash = ethers.hexlify(rng.bytes(32));
const sig = genuine(1, pk, messageHash, rng);
// random status: 0 ACTIVE, 1 DEPRECATED, 2 REVOKED
const pick = rng.int(3);
let status = Status.ACTIVE;
if (pick === 1) {
await reg.setStatus(id, Status.DEPRECATED);
status = Status.DEPRECATED;
} else if (pick === 2) {
await reg.setStatus(id, Status.REVOKED);
status = Status.REVOKED;
}
rows.push({ id, pk, messageHash, sig, status });
}
for (const r of rows) {
const expected = r.status !== Status.REVOKED; // ACTIVE or DEPRECATED verify; REVOKED never
expect(await reg.verify(r.id, r.pk, r.messageHash, r.sig)).to.equal(expected);
// A tampered signature is always false regardless of status.
expect(await reg.verify(r.id, r.pk, r.messageHash, tamperCommitment(1, r.sig))).to.equal(false);
// Revoked rows are never usable/active.
if (r.status === Status.REVOKED) {
expect(await reg.isUsable(r.id)).to.equal(false);
expect(await reg.isActive(r.id)).to.equal(false);
}
}
// Unknown ids are always false.
expect(await reg.verify(N + 100, rows[0].pk, rows[0].messageHash, rows[0].sig)).to.equal(false);
});
it("adding N algorithms keeps ids monotonic and counts consistent", async function () {
const rng = makeRng("fuzz-monotonic-v1");
const N = 24;
let prev = Number(await reg.algorithmCount());
for (let i = 0; i < N; i++) {
const sig = rng.int(2) === 0;
const args = sig
? falcon512Args("A-" + i)
: ["H-" + i, 9, PRECOMPILE[1], 0, 0, 0x00, rng.int(1000000), false];
const id = await addAlgo(args);
expect(id).to.equal(prev + 1); // monotonic, +1 each time
expect(Number(await reg.algorithmCount())).to.equal(id); // count == last id
prev = id;
}
});
it("resolveActive always terminates (returns an ACTIVE id or reverts) over random graphs", async function () {
const rng = makeRng("fuzz-resolve-v1");
const M = 12;
const ids = [];
for (let i = 0; i < M; i++) ids.push(await addAlgo(falcon512Args("G-" + i)));
// Randomly deprecate ~2/3 of them (leave the rest ACTIVE).
const status = {};
for (const id of ids) {
if (rng.int(3) !== 0) {
await reg.setStatus(id, Status.DEPRECATED);
status[id] = Status.DEPRECATED;
} else {
status[id] = Status.ACTIVE;
}
}
// Random successor edges (may form cycles); skip self-edges.
const succ = {};
for (const id of ids) {
if (rng.int(2) === 0) {
let t = ids[rng.int(M)];
if (t !== id) {
await reg.setSuccessor(id, t);
succ[id] = t;
}
}
}
// Off-chain reference walk: what resolveActive SHOULD do.
function refResolve(start) {
let cur = start;
const seen = new Set();
while (true) {
if (status[cur] === Status.ACTIVE) return cur;
if (seen.has(cur)) return "cycle";
seen.add(cur);
const n = succ[cur];
if (n === undefined) return "dead";
cur = n;
}
}
for (const id of ids) {
const ref = refResolve(id);
if (typeof ref === "number") {
const got = Number(await reg.resolveActive(id));
expect(got).to.equal(ref);
expect(await reg.isActive(got)).to.equal(true);
} else {
// dead-end or cycle -> must revert (and thus terminate, not hang).
let reverted = false;
try {
await reg.resolveActive(id);
} catch (e) {
reverted = true;
}
expect(reverted).to.equal(true);
}
}
});
});
// -----------------------------------------------------------------------
// Gas measurement (informational)
// -----------------------------------------------------------------------
describe("gas", function () {
it("reports deploy / seed / verify / admin gas", async function () {
const R = await ethers.getContractFactory("AereCryptoRegistry");
const fresh = await R.deploy();
const deployRc = await fresh.deploymentTransaction().wait();
const seedRc = await (await fresh.seedLiveSchemes()).wait();
const rng = makeRng("gas");
const pkF = makePubKey(2, rng);
const msg = ethers.keccak256(ethers.toUtf8Bytes("gas"));
const sigF = genuine(2, pkF, msg, rng);
const pkM = makePubKey(3, rng);
const sigM = genuine(3, pkM, msg, rng);
const gVerifyFalcon = await fresh.verify.estimateGas(2, pkF, msg, sigF);
const gVerifyMldsa = await fresh.verify.estimateGas(3, pkM, msg, sigM);
const gAdd = await fresh.addAlgorithm.estimateGas(...falcon512Args("Falcon-512-v2"));
const addRc = await (await fresh.addAlgorithm(...falcon512Args("Falcon-512-v2"))).wait();
const gStatus = await fresh.setStatus.estimateGas(6, Status.DEPRECATED);
const gSucc = await fresh.setSuccessor.estimateGas(1, 6);
console.log(" gas — deploy :", deployRc.gasUsed.toString());
console.log(" gas — seedLiveSchemes :", seedRc.gasUsed.toString());
console.log(" gas — verify (Falcon-1024, mock):", gVerifyFalcon.toString());
console.log(" gas — verify (ML-DSA-44, mock) :", gVerifyMldsa.toString());
console.log(" gas — addAlgorithm :", gAdd.toString(), "(actual", addRc.gasUsed.toString() + ")");
console.log(" gas — setStatus :", gStatus.toString());
console.log(" gas — setSuccessor :", gSucc.toString());
expect(deployRc.gasUsed).to.be.greaterThan(0n);
});
});
});