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.
436 lines
21 KiB
JavaScript
436 lines
21 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers, network } = require("hardhat");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AereThresholdPQCRegistry -- on-chain t-of-n POST-QUANTUM threshold authorization.
|
|
//
|
|
// A committee of n PQC signers each hold their own NIST PQC key. A valid authorization
|
|
// requires >= t DISTINCT members to each PQC-sign the SAME domain-separated challenge,
|
|
// with every leg verified on-chain by the scheme's live precompile (0x0AE1..0x0AE4).
|
|
//
|
|
// HONEST label: this is a threshold MULTISIG of independent PQC signatures, NOT a single
|
|
// aggregate threshold-PQC signature (that is open research; see docs/THRESHOLD-PQC-*.md).
|
|
//
|
|
// Precompile modelling: Hardhat has no PQC precompile, so we install the repo's faithful
|
|
// MockPQCPrecompile at 0x0AE1..0x0AE4 (hardhat_setCode). It parses the input at the
|
|
// precompiles' SPEC offsets and accepts iff commitment == keccak256(pk || message) -- the
|
|
// same model the shipped AerePQCAttestation adversarial suite uses. Because the mock parses
|
|
// at the spec offsets (not offsets copied from the contract), a passing authorize() proves
|
|
// BOTH the threshold-counting logic AND that the contract built the precompile envelope at
|
|
// the correct offsets. The REAL envelope is separately proven bit-for-bit against the LIVE
|
|
// mainnet precompile via eth_call (scripts/pqc/verify-live-precompile.js).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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
|
|
};
|
|
|
|
const META = {
|
|
1: { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, falcon: true },
|
|
2: { pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, falcon: true },
|
|
3: { pkLen: 1312, sigLen: 2420, falcon: false },
|
|
4: { pkLen: 32, sigLen: 7856, falcon: false },
|
|
};
|
|
|
|
// Deterministic seeded RNG (keccak hash-chain) so the whole suite is 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;
|
|
}
|
|
return { bytes };
|
|
}
|
|
|
|
function makePubKey(scheme, rng) {
|
|
const m = META[scheme];
|
|
const raw = rng.bytes(m.pkLen);
|
|
if (m.falcon) raw[0] = m.pkHeader; // Falcon requires the header byte
|
|
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 a signature envelope carrying 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 by `pubKey` over `message`.
|
|
function genuine(scheme, pubKey, message, rng) {
|
|
return envelope(scheme, commitmentFor(pubKey, message), rng);
|
|
}
|
|
|
|
// Flip one byte inside the commitment region -> a genuine forgery the precompile rejects.
|
|
function tamperCommitment(scheme, sigHex) {
|
|
const b = ethers.getBytes(sigHex);
|
|
const idx = META[scheme].falcon ? 41 : 0;
|
|
b[idx] ^= 0x01;
|
|
return ethers.hexlify(b);
|
|
}
|
|
|
|
describe("AereThresholdPQCRegistry (post-quantum threshold multisig)", function () {
|
|
let reg, mockCode;
|
|
|
|
before(async function () {
|
|
// 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("AereThresholdPQCRegistry");
|
|
reg = await R.deploy();
|
|
await reg.waitForDeployment();
|
|
});
|
|
|
|
// Register a fresh n-member committee of `scheme`, threshold t. Returns the committee id,
|
|
// its member public keys, and a shared rng.
|
|
async function registerCommittee(scheme, n, t, seed) {
|
|
const rng = makeRng(seed || `c-${scheme}-${n}-${t}`);
|
|
const pubKeys = [];
|
|
for (let i = 0; i < n; i++) pubKeys.push(makePubKey(scheme, rng));
|
|
const tx = await reg.registerCommittee(scheme, t, pubKeys);
|
|
const rc = await tx.wait();
|
|
const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "CommitteeRegistered");
|
|
return { id: Number(log.args.committeeId), pubKeys, rng, scheme, n, t };
|
|
}
|
|
|
|
// Build the `sigs` array of {memberIndex, signature} for the given member indices, each a
|
|
// genuine PQC signature over the committee's current authorization challenge.
|
|
async function legsFor(c, messageHash, memberIndices) {
|
|
const nonce = await reg.nonceOf(c.id);
|
|
const challenge = await reg.authChallenge(c.id, nonce, messageHash);
|
|
return memberIndices.map((idx) => ({
|
|
memberIndex: idx,
|
|
signature: genuine(c.scheme, c.pubKeys[idx], challenge, c.rng),
|
|
}));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Registration validation
|
|
// -----------------------------------------------------------------------
|
|
describe("registerCommittee", function () {
|
|
it("registers a committee for every scheme and stores scheme/t/n + members commitment", async function () {
|
|
for (const scheme of [1, 2, 3, 4]) {
|
|
const c = await registerCommittee(scheme, 3, 2, `reg-${scheme}`);
|
|
const got = await reg.getCommittee(c.id);
|
|
expect(got.exists).to.equal(true);
|
|
expect(Number(got.scheme)).to.equal(scheme);
|
|
expect(Number(got.threshold)).to.equal(2);
|
|
expect(Number(got.size)).to.equal(3);
|
|
expect(Number(got.authNonce)).to.equal(0);
|
|
// members commitment matches abi.encode(pubKeys)
|
|
const expectHash = ethers.keccak256(
|
|
ethers.AbiCoder.defaultAbiCoder().encode(["bytes[]"], [c.pubKeys]),
|
|
);
|
|
expect(got.membersHash).to.equal(expectHash);
|
|
// stored keys are retrievable
|
|
expect(await reg.getMemberPubKey(c.id, 0)).to.equal(c.pubKeys[0]);
|
|
expect(await reg.precompileFor(scheme)).to.equal(PRECOMPILE[scheme]);
|
|
}
|
|
});
|
|
|
|
it("rejects bad committee params (t=0, t>n, empty, n>255)", async function () {
|
|
const rng = makeRng("bad");
|
|
const three = [0, 1, 2].map(() => makePubKey(1, rng));
|
|
await expect(reg.registerCommittee(1, 0, three)).to.be.revertedWithCustomError(reg, "BadCommitteeParams");
|
|
await expect(reg.registerCommittee(1, 4, three)).to.be.revertedWithCustomError(reg, "BadCommitteeParams");
|
|
await expect(reg.registerCommittee(1, 1, [])).to.be.revertedWithCustomError(reg, "BadCommitteeParams");
|
|
});
|
|
|
|
it("rejects an unknown scheme and malformed member keys", async function () {
|
|
const rng = makeRng("mal");
|
|
const goodF = makePubKey(1, rng);
|
|
// unknown scheme
|
|
await expect(reg.registerCommittee(5, 1, [goodF])).to.be.revertedWithCustomError(reg, "InvalidScheme");
|
|
// Falcon-512 wrong header
|
|
const badHeader = ethers.getBytes(makePubKey(1, rng));
|
|
badHeader[0] = 0x0a;
|
|
await expect(reg.registerCommittee(1, 1, [ethers.hexlify(badHeader)])).to.be.revertedWithCustomError(
|
|
reg,
|
|
"InvalidPubKey",
|
|
);
|
|
// ML-DSA-44 wrong length
|
|
await expect(reg.registerCommittee(3, 1, ["0x" + "00".repeat(1311)])).to.be.revertedWithCustomError(
|
|
reg,
|
|
"InvalidPubKey",
|
|
);
|
|
// the offending member index is reported
|
|
await expect(reg.registerCommittee(1, 2, [goodF, "0x" + "09" + "00".repeat(10)]))
|
|
.to.be.revertedWithCustomError(reg, "InvalidPubKey")
|
|
.withArgs(1);
|
|
});
|
|
|
|
it("rejects a committee with duplicate keys (distinctness is by KEY, not index)", async function () {
|
|
// Authorization counts distinct signers by member index, so a repeated key would let one
|
|
// keyholder fill multiple slots and reach threshold alone. registerCommittee must reject it.
|
|
const rng = makeRng("dup-key-registry");
|
|
const keys = [];
|
|
for (let i = 0; i < 4; i++) keys.push(makePubKey(1, rng));
|
|
keys[2] = keys[0]; // member 2 reuses member 0's key
|
|
await expect(reg.registerCommittee(1, 2, keys))
|
|
.to.be.revertedWithCustomError(reg, "DuplicatePubKey")
|
|
.withArgs(2);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// t-of-n happy path + threshold gate
|
|
// -----------------------------------------------------------------------
|
|
describe("authorize: t-of-n happy path and threshold gate", function () {
|
|
it("authorizes with EXACTLY t distinct valid PQC legs (2-of-3, Falcon-512)", async function () {
|
|
const c = await registerCommittee(1, 3, 2, "happy-f512");
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("release vault payment #1"));
|
|
const sigs = await legsFor(c, messageHash, [0, 2]); // any 2 distinct members
|
|
|
|
const before = await reg.authorizationCount();
|
|
await expect(reg.authorize(c.id, messageHash, sigs))
|
|
.to.emit(reg, "Authorized")
|
|
.withArgs(c.id, 0, messageHash, await reg.authChallenge(c.id, 0, messageHash), 2, anyBlock());
|
|
expect(await reg.authorizationCount()).to.equal(before + 1n);
|
|
expect(Number(await reg.nonceOf(c.id))).to.equal(1);
|
|
|
|
const a = await reg.getAuthorization(c.id, 0);
|
|
expect(a.exists).to.equal(true);
|
|
expect(a.messageHash).to.equal(messageHash);
|
|
expect(Number(a.quorum)).to.equal(2);
|
|
expect(await reg.isAuthorized(c.id, 0, messageHash)).to.equal(true);
|
|
expect(await reg.isAuthorized(c.id, 0, ethers.ZeroHash)).to.equal(false);
|
|
});
|
|
|
|
it("authorizes for EVERY scheme with a t-of-n quorum", async function () {
|
|
for (const scheme of [1, 2, 3, 4]) {
|
|
const c = await registerCommittee(scheme, 4, 3, `all-${scheme}`);
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes(`msg-${scheme}`));
|
|
const sigs = await legsFor(c, messageHash, [0, 1, 3]);
|
|
await expect(reg.authorize(c.id, messageHash, sigs)).to.emit(reg, "Authorized");
|
|
expect(Number(await reg.nonceOf(c.id))).to.equal(1);
|
|
}
|
|
});
|
|
|
|
it("REJECTS a below-threshold quorum (t-1 valid legs)", async function () {
|
|
const c = await registerCommittee(1, 3, 2, "below");
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("only one signer"));
|
|
const sigs = await legsFor(c, messageHash, [1]); // 1 < t=2
|
|
await expect(reg.authorize(c.id, messageHash, sigs))
|
|
.to.be.revertedWithCustomError(reg, "BelowThreshold")
|
|
.withArgs(1, 2);
|
|
// nothing recorded, nonce frozen
|
|
expect(await reg.authorizationCount()).to.equal(0n);
|
|
expect(Number(await reg.nonceOf(c.id))).to.equal(0);
|
|
expect((await reg.getAuthorization(c.id, 0)).exists).to.equal(false);
|
|
});
|
|
|
|
it("previewAuthorize reports quorum without recording (lenient view)", async function () {
|
|
const c = await registerCommittee(2, 5, 3, "preview");
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("preview-msg"));
|
|
const nonce = await reg.nonceOf(c.id);
|
|
const three = await legsFor(c, messageHash, [0, 2, 4]);
|
|
const two = three.slice(0, 2);
|
|
expect((await reg.previewAuthorize(c.id, nonce, messageHash, three)).would).to.equal(true);
|
|
expect((await reg.previewAuthorize(c.id, nonce, messageHash, two)).would).to.equal(false);
|
|
// view did not mutate state
|
|
expect(Number(await reg.nonceOf(c.id))).to.equal(0);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Distinctness, membership, message binding, replay
|
|
// -----------------------------------------------------------------------
|
|
describe("authorize: distinctness / membership / binding / replay", function () {
|
|
it("does NOT double-count a duplicate member (reverts DuplicateMember)", async function () {
|
|
const c = await registerCommittee(1, 3, 2, "dup");
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("dup-msg"));
|
|
const nonce = await reg.nonceOf(c.id);
|
|
const challenge = await reg.authChallenge(c.id, nonce, messageHash);
|
|
// member 0 signs twice: two legs, both valid, but same index -> not a 2-of-3 quorum
|
|
const m0a = { memberIndex: 0, signature: genuine(c.scheme, c.pubKeys[0], challenge, c.rng) };
|
|
const m0b = { memberIndex: 0, signature: genuine(c.scheme, c.pubKeys[0], challenge, c.rng) };
|
|
await expect(reg.authorize(c.id, messageHash, [m0a, m0b]))
|
|
.to.be.revertedWithCustomError(reg, "DuplicateMember")
|
|
.withArgs(0);
|
|
expect(await reg.authorizationCount()).to.equal(0n);
|
|
});
|
|
|
|
it("rejects an out-of-range member index (non-member)", async function () {
|
|
const c = await registerCommittee(1, 3, 2, "oor");
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("oor-msg"));
|
|
const good = await legsFor(c, messageHash, [0]);
|
|
const bogus = { memberIndex: 9, signature: good[0].signature };
|
|
await expect(reg.authorize(c.id, messageHash, [good[0], bogus]))
|
|
.to.be.revertedWithCustomError(reg, "MemberOutOfRange")
|
|
.withArgs(9);
|
|
});
|
|
|
|
it("rejects a leg signed by a NON-member key at a valid index", async function () {
|
|
const c = await registerCommittee(1, 3, 2, "foreign");
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("foreign-msg"));
|
|
const nonce = await reg.nonceOf(c.id);
|
|
const challenge = await reg.authChallenge(c.id, nonce, messageHash);
|
|
const outsiderKey = makePubKey(1, c.rng); // never registered
|
|
const legGood = { memberIndex: 0, signature: genuine(c.scheme, c.pubKeys[0], challenge, c.rng) };
|
|
const legForeign = { memberIndex: 1, signature: genuine(c.scheme, outsiderKey, challenge, c.rng) };
|
|
await expect(reg.authorize(c.id, messageHash, [legGood, legForeign]))
|
|
.to.be.revertedWithCustomError(reg, "LegVerificationFailed")
|
|
.withArgs(1);
|
|
});
|
|
|
|
it("rejects a leg that is a tampered / invalid PQC signature", async function () {
|
|
const c = await registerCommittee(2, 3, 2, "tamper");
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("tamper-msg"));
|
|
const sigs = await legsFor(c, messageHash, [0, 1]);
|
|
sigs[1].signature = tamperCommitment(c.scheme, sigs[1].signature);
|
|
await expect(reg.authorize(c.id, messageHash, sigs))
|
|
.to.be.revertedWithCustomError(reg, "LegVerificationFailed")
|
|
.withArgs(1);
|
|
});
|
|
|
|
it("rejects legs that are genuine over a DIFFERENT message", async function () {
|
|
const c = await registerCommittee(1, 3, 2, "wrongmsg");
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("intended"));
|
|
const otherHash = ethers.keccak256(ethers.toUtf8Bytes("attacker-substituted"));
|
|
// sign the challenge for `otherHash`, then submit against `messageHash`
|
|
const nonce = await reg.nonceOf(c.id);
|
|
const otherChallenge = await reg.authChallenge(c.id, nonce, otherHash);
|
|
const sigs = [0, 1].map((idx) => ({
|
|
memberIndex: idx,
|
|
signature: genuine(c.scheme, c.pubKeys[idx], otherChallenge, c.rng),
|
|
}));
|
|
await expect(reg.authorize(c.id, messageHash, sigs)).to.be.revertedWithCustomError(reg, "LegVerificationFailed");
|
|
expect(await reg.authorizationCount()).to.equal(0n);
|
|
});
|
|
|
|
it("prevents REPLAY of a used authorization (nonce advances, same sigs no longer verify)", async function () {
|
|
const c = await registerCommittee(1, 3, 2, "replay");
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("pay once"));
|
|
const sigs = await legsFor(c, messageHash, [0, 1]); // legs bound to nonce 0
|
|
await reg.authorize(c.id, messageHash, sigs); // consumes nonce 0
|
|
expect(Number(await reg.nonceOf(c.id))).to.equal(1);
|
|
// exact replay: challenge now uses nonce 1, so the nonce-0 legs fail verification
|
|
await expect(reg.authorize(c.id, messageHash, sigs)).to.be.revertedWithCustomError(reg, "LegVerificationFailed");
|
|
expect(await reg.authorizationCount()).to.equal(1n);
|
|
expect(Number(await reg.nonceOf(c.id))).to.equal(1);
|
|
});
|
|
|
|
it("allows a fresh authorization at the new nonce after a successful one", async function () {
|
|
const c = await registerCommittee(3, 4, 3, "second");
|
|
const msg1 = ethers.keccak256(ethers.toUtf8Bytes("first"));
|
|
await reg.authorize(c.id, msg1, await legsFor(c, msg1, [0, 1, 2]));
|
|
expect(Number(await reg.nonceOf(c.id))).to.equal(1);
|
|
const msg2 = ethers.keccak256(ethers.toUtf8Bytes("second"));
|
|
await reg.authorize(c.id, msg2, await legsFor(c, msg2, [1, 2, 3]));
|
|
expect(Number(await reg.nonceOf(c.id))).to.equal(2);
|
|
expect(await reg.isAuthorized(c.id, 0, msg1)).to.equal(true);
|
|
expect(await reg.isAuthorized(c.id, 1, msg2)).to.equal(true);
|
|
});
|
|
|
|
it("reverts on an unknown committee", async function () {
|
|
await expect(reg.authorize(999, ethers.ZeroHash, [])).to.be.revertedWithCustomError(reg, "UnknownCommittee");
|
|
});
|
|
|
|
it("treats an empty (pre-fork / wiped) precompile as an invalid leg, not an authorization", async function () {
|
|
const c = await registerCommittee(2, 3, 2, "wiped");
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes("wiped-msg"));
|
|
const sigs = await legsFor(c, messageHash, [0, 1]);
|
|
await network.provider.send("hardhat_setCode", [PRECOMPILE[2], "0x"]); // wipe Falcon-1024 precompile
|
|
await expect(reg.authorize(c.id, messageHash, sigs)).to.be.revertedWithCustomError(reg, "LegVerificationFailed");
|
|
expect(await reg.authorizationCount()).to.equal(0n);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Gas ceiling under EIP-7825: measure the marginal cost per additional leg.
|
|
// NB: the mock verify is cheaper than the live precompile's FIXED verify gas
|
|
// (Falcon-512 40k, Falcon-1024 75k, ML-DSA-44 55k, SLH-DSA-128s 350k). So the
|
|
// REAL per-leg cost = (measured marginal here, which is calldata + envelope build +
|
|
// distinct-member bookkeeping + cheap mock verify) + the precompile's fixed verify gas.
|
|
// This test reports the Solidity/calldata overhead per leg so the ceiling in
|
|
// docs/THRESHOLD-PQC-2026-07-12.md is grounded in a measurement, not a guess.
|
|
// -----------------------------------------------------------------------
|
|
describe("gas: per-leg marginal cost (informs the EIP-7825 (scheme, t) ceiling)", function () {
|
|
const CAP = 16_777_216n;
|
|
const PRECOMPILE_VERIFY_GAS = { 1: 40000n, 2: 75000n, 3: 55000n, 4: 350000n };
|
|
|
|
async function authGas(scheme, t, seed) {
|
|
const n = t; // exactly-t quorum
|
|
const c = await registerCommittee(scheme, n, t, seed);
|
|
const messageHash = ethers.keccak256(ethers.toUtf8Bytes(seed));
|
|
const sigs = await legsFor(
|
|
c,
|
|
messageHash,
|
|
Array.from({ length: t }, (_, i) => i),
|
|
);
|
|
const tx = await reg.authorize(c.id, messageHash, sigs);
|
|
const rc = await tx.wait();
|
|
return rc.gasUsed;
|
|
}
|
|
|
|
it("reports marginal Solidity+calldata overhead per leg and the projected real ceiling", async function () {
|
|
for (const scheme of [1, 2, 3, 4]) {
|
|
const g2 = await authGas(scheme, 2, `gas-${scheme}-2`);
|
|
const g6 = await authGas(scheme, 6, `gas-${scheme}-6`);
|
|
const overheadPerLeg = (g6 - g2) / 4n; // 4 extra legs between t=2 and t=6
|
|
|
|
// Real per-leg = measured Solidity/calldata overhead (which already includes the
|
|
// cheap mock verify) + the precompile's fixed verify gas. Slightly conservative
|
|
// (double-counts the tiny mock verify, so the ceiling below is an under-estimate).
|
|
const realPerLeg = overheadPerLeg + PRECOMPILE_VERIFY_GAS[scheme];
|
|
const fixedOverhead = g2 - overheadPerLeg * 2n; // tx base + committee load + record
|
|
const tMax = (CAP - fixedOverhead) / realPerLeg;
|
|
|
|
console.log(
|
|
` scheme ${scheme}: mock authorize t=2 ${g2} gas, t=6 ${g6} gas; ` +
|
|
`Solidity/calldata overhead/leg ~${overheadPerLeg}; ` +
|
|
`+precompile ${PRECOMPILE_VERIFY_GAS[scheme]} => real/leg ~${realPerLeg}; ` +
|
|
`projected t_max under 2^24 ~= ${tMax}`,
|
|
);
|
|
|
|
// Sanity: the per-leg cost is positive and the projected ceiling is a real bound.
|
|
expect(overheadPerLeg > 0n).to.equal(true);
|
|
expect(tMax > 0n).to.equal(true);
|
|
// Falcon-512 (cheapest verify) must admit a larger quorum than SLH-DSA-128s.
|
|
if (scheme === 1) expect(tMax > 20n).to.equal(true);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// chai matcher helper: accept any block number in withArgs.
|
|
function anyBlock() {
|
|
return (x) => typeof x === "bigint" || typeof x === "number";
|
|
}
|