fast-time: lib/override.mjs (the override as text: duplicate keys collapsed, keys dropped by line, the binary probed for the keys it does not know and those dropped, one prune set for every file of a case); the crossing and digest harnesses take it (the first run at 18:47Z: fork 546fe4b5, on release-0.3.23-node 43360992, refuses the 60x file's sig_scheme and three later keys that master's newest fork line added; the digest harness no longer imports the crossing harness, which would have run it)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 18:49:32 +00:00
parent 4aacf350e7
commit cd380cefb5
3 changed files with 77 additions and 25 deletions

View file

@ -38,6 +38,7 @@ import { spawn, spawnSync } from 'node:child_process';
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync, appendFileSync, copyFileSync } from 'node:fs';
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
import { devAddress } from '../../tools/harness/lib/address.mjs';
import { mergeOverrideText, probeUnknownKeys } from './lib/override.mjs';
const ROOT = new URL('../../', import.meta.url).pathname;
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
@ -85,26 +86,13 @@ try { for (const pid of readFileSync(PIDS, 'utf8').split('\n').map(Number).filte
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
const recordPid = (p) => { try { appendFileSync(PIDS, `${p.pid}\n`); } catch { } };
// the override: merged as TEXT (a never height does not survive a JavaScript number); duplicated top-level keys are
// collapsed to their last copy first (the 18:53 UK merge into class-v5 doubled six keys and the node refuses a duplicate);
// a null field is dropped (an unset Option; an older binary refuses the unknown key)
// the override: merged as TEXT by lib/override.mjs (a never height does not survive a JavaScript number; duplicated
// top-level keys collapsed; a null field dropped); then the binary is probed and every key it does not know is dropped
const baseText = readFileSync(FILE, 'utf8');
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
const EPOCH = field('pow_epoch_blocks');
const LEAD = field('pow_epoch_lead');
const DAY_MS = field('pow_day_ms');
export function dedupeTopLevel(text) {
const lines = text.split('\n'); const seen = new Set(); const keep = lines.map(() => true);
for (let i = lines.length - 1; i >= 0; i--) { const m = /^ "([a-z0-9_]+)":/.exec(lines[i]); if (!m) continue; if (seen.has(m[1])) keep[i] = false; else seen.add(m[1]); }
return lines.filter((_, i) => keep[i]).join('\n');
}
export function mergeOverrideText(text, fields) {
let out = dedupeTopLevel(text);
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
out = out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
return out.replace(/\n\s*"[a-z0-9_]+":\s*null\s*,?/g, '').replace(/,(\s*})/g, '$1');
}
const asText = (v) => v == null ? NEVER : String(v);
const override = `${TMP}/override.json`;
writeFileSync(override, mergeOverrideText(baseText, {
@ -113,12 +101,14 @@ writeFileSync(override, mergeOverrideText(baseText, {
program_class_v5_activation_daa: asText(FLOOR),
latency_ladder_activation_daa: '0', latency_ladder_window_daa: String(LADDER_WINDOW),
}));
const DROPPED = await probeUnknownKeys(IGNEUMD, override, { tmp: `${TMP}/probe`, port: BASE + 90, suffix: SUFFIX, log: (m) => console.log(`${new Date().toISOString().slice(11, 23)} ${m}`) });
// the first epoch whose seed block (the last chain block below L e - lead) can have seven full windows below it
const firstFull = (w) => { let e = 1; while (e * EPOCH - LEAD - 1 < WINDOWS * w) e++; return e; };
const FIRST_STEP_EPOCH = firstFull(LADDER_WINDOW);
const FIRST_FLIP_EPOCH = Math.max(firstFull(CLASS_WINDOW), 1); // the previous epoch must be v4: from genesis here
const FLOOR_EPOCH = FLOOR == null ? null : Math.ceil(FLOOR / EPOCH);
const BOUNDARY = FIRST_FLIP_EPOCH * EPOCH;
log(`override: ${DROPPED.length ? `${DROPPED.length} key(s) the binary does not know dropped (${DROPPED.join(', ')})` : 'every key known to the binary'}`);
log(`case ${CASE}: ${N} nodes, bytes ${SIGNAL.join('/')}, ladder ${LADDER.join('/')}; class v4 sub-version 3 from genesis at rung 0; class window ${CLASS_WINDOW} DAA x ${WINDOWS} (the first epoch that can cross to v5 is ${FIRST_FLIP_EPOCH}, DAA ${BOUNDARY}), ladder window ${LADDER_WINDOW} DAA x ${WINDOWS} (the first epoch that can step is ${FIRST_STEP_EPOCH}, DAA ${FIRST_STEP_EPOCH * EPOCH}); v5 floor ${FLOOR ?? 'never'} (epoch ${FLOOR_EPOCH ?? 'none'}); stale node ${STALE ?? 'none'}; restart node ${RESTART ?? 'none'} (stop at DAA ${BOUNDARY - RESTART_BEFORE}, start at ${BOUNDARY + RESTART_AFTER}); ${EPOCH} DAA per epoch, lead ${LEAD}; run ${SECS} s or ${EPOCHS} epochs; node ${IGNEUMD}`);
// the CPU verifier reproduces the first class v5 pack's id for Devnet 3's genesis seed (the pack carries its stream)
@ -421,7 +411,7 @@ const pass = Object.values(checks).every(v => v === true || v === null);
const summary = {
pass, case: CASE, utc: new Date().toISOString(), checks, signals: SIGNAL, ladder: LADDER, class_window: CLASS_WINDOW, ladder_window: LADDER_WINDOW, windows: WINDOWS, class_threshold_bps: CLASS_THRESHOLD, ladder_threshold_bps: LADDER_THRESHOLD,
floor: FLOOR ?? 'never', floor_epoch: FLOOR_EPOCH, epoch_blocks: EPOCH, lead: LEAD, first_step_epoch: FIRST_STEP_EPOCH, first_flip_epoch: FIRST_FLIP_EPOCH, boundary_daa: BOUNDARY,
node: IGNEUMD, miner: CPU_MINER, pow: IGNEUM_POW, dn3_genesis_check: dn3, ladder_step: step, class_switch: flip, run_ended_at_s: endAt, final_daa: lastDaa, max_epoch_seen: maxEpochSeen,
node: IGNEUMD, miner: CPU_MINER, pow: IGNEUM_POW, override_keys_dropped: DROPPED, dn3_genesis_check: dn3, ladder_step: step, class_switch: flip, run_ended_at_s: endAt, final_daa: lastDaa, max_epoch_seen: maxEpochSeen,
epochs: Object.fromEntries(epochRows), streams: Object.fromEntries([...streams.entries()].map(([e, s]) => [e, { root: s.root, records: s.records, block: s.block, roots_per_node: s.roots }])),
blocks: { total: blocks.length, before_boundary: before.length, after_boundary: after.length, object_bytes: objectBytes, byte_6_share_bps: byte6Share, ladder_bits: ladderBits, up_share_bps: upShare },
program_id_rows: idRows, accepted_per_miner: accepted, accepted_after_flip_per_miner: acceptedAfterFlip, rejected_by_miners: rejectedMiner, rejected_by_nodes: rejectedNode,

View file

@ -22,7 +22,7 @@
import { spawn } from 'node:child_process';
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync, appendFileSync, copyFileSync } from 'node:fs';
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
import { dedupeTopLevel } from './class-v5-crossing.mjs';
import { mergeOverrideText, dropKeys, probeUnknownKeys } from './lib/override.mjs';
const ROOT = new URL('../../', import.meta.url).pathname;
const NODE_ROOT = process.env.IGNEUM_NODE_ROOT || ROOT;
@ -53,19 +53,18 @@ const baseText = readFileSync(FILE, 'utf8');
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
const DAY_MS = field('pow_day_ms');
const NEVER = '18446744073709551615';
function merge(text, fields, drop = []) {
let out = dedupeTopLevel(text);
for (const k of [...Object.keys(fields), ...drop]) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
out = out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
return out.replace(/\n\s*"[a-z0-9_]+":\s*null\s*,?/g, '').replace(/,(\s*})/g, '$1');
}
const merge = (text, fields, drop = []) => dropKeys(mergeOverrideText(text, fields), drop);
const common = { genesis_bits: GENESIS_BITS, skip_proof_of_work: false, program_class_v3_activation_daa: '0', program_class_v4_activation_daa: '0', program_class_v4_signal_window_daa: '60', latency_ladder_activation_daa: '0', latency_ladder_window_daa: '45' };
const fileA = `${TMP}/override-a-absent.json`, fileB = `${TMP}/override-b-never.json`, fileC = `${TMP}/override-c-set.json`;
writeFileSync(fileA, merge(baseText, common, ['program_class_v5_activation_daa']));
writeFileSync(fileB, merge(baseText, { ...common, program_class_v5_activation_daa: NEVER }));
writeFileSync(fileC, merge(baseText, { ...common, program_class_v5_activation_daa: String(FLOOR) }));
if (/program_class_v5_activation_daa/.test(readFileSync(fileA, 'utf8'))) { console.error('file A still carries the v5 key'); process.exit(2); }
// the keys the OLD binary (and then the new one) refuses are dropped from EVERY file, so the three digests differ by the v5 field alone
const plog = (m) => console.log(`${new Date().toISOString().slice(11, 23)} ${m}`);
const DROPPED = [...await probeUnknownKeys(OLD, fileA, { tmp: `${TMP}/probe`, port: BASE + 90, suffix: SUFFIX, log: plog }), ...await probeUnknownKeys(NEW, fileA, { tmp: `${TMP}/probe`, port: BASE + 90, suffix: SUFFIX, log: plog })];
for (const f of [fileB, fileC]) writeFileSync(f, dropKeys(readFileSync(f, 'utf8'), DROPPED));
log(`override: ${DROPPED.length ? `${DROPPED.length} key(s) dropped from every file (${DROPPED.join(', ')})` : 'every key known to both binaries'}`);
log(`case ${CASE}: new ${NEW}, old ${OLD}; A = the v5 key absent, B = at never, C = ${FLOOR}; ${SECS} s of mixed mining`);
const started = [];
@ -164,7 +163,7 @@ const checks = {
every_mined_header_is_block_version_2_object_byte_7: blocks.length > 1 && Object.keys(versions).every(v => (+v & 0xff) === 2 && ((+v >> 8) & 0x3f) === 7),
};
const pass = Object.values(checks).every(Boolean);
const summary = { pass, case: CASE, utc: new Date().toISOString(), checks, new: NEW, old: OLD, floor: FLOOR, secs: SECS, digests: digests.map(d => d?.slice(0, 16)), peers, refusal_lines: { old: refusal(n2).slice(0, 2), set: refusal(n3).slice(0, 2), hub: refusal(n0).slice(0, 2) }, accepted, rejected, sinks, counts, header_versions: versions, object_bytes: objectBytes, blocks: blocks.length, samples };
const summary = { pass, case: CASE, utc: new Date().toISOString(), checks, new: NEW, old: OLD, override_keys_dropped: DROPPED, floor: FLOOR, secs: SECS, digests: digests.map(d => d?.slice(0, 16)), peers, refusal_lines: { old: refusal(n2).slice(0, 2), set: refusal(n3).slice(0, 2), hub: refusal(n0).slice(0, 2) }, accepted, rejected, sinks, counts, header_versions: versions, object_bytes: objectBytes, blocks: blocks.length, samples };
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
try { mkdirSync(OUT, { recursive: true }); writeFileSync(`${OUT}/${CASE}.json`, JSON.stringify(summary, null, 2)); } catch { }
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'} (${CASE}) ${summary.utc}: digests new-absent ${digests[0]?.slice(0, 16)} new-never ${digests[1]?.slice(0, 16)} old-absent ${digests[2]?.slice(0, 16)} new-set ${digests[3]?.slice(0, 16)}; peers ${peers.join('/')}; refusal lines old ${refusal(n2).length} set ${refusal(n3).length} hub ${refusal(n0).length}; mixed mining ${SECS} s: new accepted ${accepted.new}, old accepted ${accepted.old}, rejected ${rejected.join('/')}, sinks ${sinks.slice(0, 3).join(' ')} at ${counts.slice(0, 3).join('/')}; header versions ${JSON.stringify(versions)}`);

View file

@ -0,0 +1,63 @@
// The override file as TEXT for the fast-time harnesses (7 October 2026): a never height is 18446744073709551615, which a
// JavaScript number rounds and the node refuses ("expected u64"), so nothing here goes through JSON.parse.
// dedupeTopLevel the file's top-level keys collapsed to their LAST copy (the 18:53 UK merge into class-v5 doubled six
// keys; the node refuses a duplicate key; tools/ci/override-json-check.sh guards the committed file)
// dropKeys named top-level keys removed, one line each (the file keeps every top-level key on its own line, so
// an array or object value goes with its line; the older `[^,}\n]+` regex cut an array at its first comma)
// mergeOverrideText dropKeys for the fields, then the fields appended as the last lines (digit strings raw, other strings
// quoted); a null field is dropped (an unset Option: an older binary refuses the unknown key)
// probeUnknownKeys the binary started on the file to a scratch data dir; each `unknown field \`x\`` it exits with is
// dropped from the file and the start retried, until the node stays up (then stopped) or `limit` keys
// are gone; returns the dropped keys. The repo's 60x file follows master's newest fork line, so a fork
// binary behind it (release-0.3.23-node at 43360992 lacks sig_scheme, sig_scheme_activation_daa,
// finality_succession_activation_daa, latency_ladder_cache_rung; the pre-v5 binary also lacks
// program_class_v5_activation_daa) refuses the file as written. A harness with several files applies
// the union of the dropped keys to EVERY file, so the digests it compares differ by the field under test alone.
import { spawn } from 'node:child_process';
import { mkdirSync, rmSync, readFileSync, writeFileSync, openSync } from 'node:fs';
export function dedupeTopLevel(text) {
const lines = text.split('\n'); const seen = new Set(); const keep = lines.map(() => true);
for (let i = lines.length - 1; i >= 0; i--) { const m = /^ "([a-z0-9_]+)":/.exec(lines[i]); if (!m) continue; if (seen.has(m[1])) keep[i] = false; else seen.add(m[1]); }
return lines.filter((_, i) => keep[i]).join('\n');
}
export function dropKeys(text, keys) {
const set = new Set(keys);
const out = text.split('\n').filter(l => { const m = /^ "([a-z0-9_]+)":/.exec(l); return !(m && set.has(m[1])); }).join('\n');
return out.replace(/,(\s*})\s*$/, '$1\n');
}
export function mergeOverrideText(text, fields) {
let out = dropKeys(dedupeTopLevel(text), Object.keys(fields));
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
out = out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
return out.replace(/\n\s*"[a-z0-9_]+":\s*null\s*,?/g, '').replace(/,(\s*})/g, '$1');
}
export function topLevelKeys(text) { return text.split('\n').map(l => /^ "([a-z0-9_]+)":/.exec(l)?.[1]).filter(Boolean); }
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
export async function probeUnknownKeys(bin, file, { tmp, port, suffix = 999, limit = 12, log = () => {} } = {}) {
const dropped = [];
for (let round = 0; round <= limit; round++) {
rmSync(tmp, { recursive: true, force: true }); mkdirSync(tmp, { recursive: true });
const logFile = `${tmp}/probe.log`;
const out = openSync(logFile, 'a');
const p = spawn(bin, ['--devnet', `--devnet-suffix=${suffix}`, '--nodnsseed', '--disable-upnp', '--nologfiles', `--appdir=${tmp}/data`, `--rpclisten=127.0.0.1:${port}`, `--rpclisten-json=127.0.0.1:${port + 1}`, `--listen=127.0.0.1:${port + 2}`, `--override-params-file=${file}`, '--outpeers=0', '--loglevel=warn', '--yes'], { stdio: ['ignore', out, out] });
let exited = false; p.on('exit', () => { exited = true; });
for (let i = 0; i < 40 && !exited; i++) await sleep(100);
if (!exited) {
try { p.kill('SIGINT'); } catch { }
for (let i = 0; i < 40 && !exited; i++) await sleep(100);
if (!exited) { try { p.kill('SIGKILL'); } catch { } await sleep(300); }
rmSync(tmp, { recursive: true, force: true });
return dropped;
}
const text = (() => { try { return readFileSync(logFile, 'utf8'); } catch { return ''; } })();
const m = /unknown field `([a-z0-9_]+)`/.exec(text);
if (!m) { rmSync(tmp, { recursive: true, force: true }); throw new Error(`the probe of ${bin} on ${file} exited ${p.exitCode} without an unknown-field line: ${text.split('\n').filter(l => /rror|refus|invalid|duplicate/.test(l)).slice(-2).join(' | ').slice(0, 300)}`); }
dropped.push(m[1]);
writeFileSync(file, dropKeys(readFileSync(file, 'utf8'), [m[1]]));
log(`override probe: ${bin.split('/').slice(-3).join('/')} does not know "${m[1]}": dropped from ${file.split('/').pop()}`);
}
rmSync(tmp, { recursive: true, force: true });
throw new Error(`the probe dropped ${limit} keys and the node still refuses the file`);
}