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.
660 lines
29 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|