igneum-pow and the fast-time profile from release-0.3.18 (the ladder's b0af074f and 0bc76818: v4_class_at, chain_program_shadow, the three ladder fields of the 60x file, the ladder harness and bench script), which the 0.3.18 fork builds against and master does not yet carry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
7af226d5c5
commit
0a5e743411
7 changed files with 518 additions and 6 deletions
|
|
@ -326,6 +326,11 @@ pub fn era_draw(era_bytes: &[u8], allowed: &[u8]) -> EraParams {
|
|||
let j = i + (r[i] % n as u64) as usize;
|
||||
c.swap(i, j);
|
||||
}
|
||||
// Draws 8 and 9, consumed and not used (spec 01 section 1.13.1 for `epoch_len`; `docs/design/latency-ladder.md`
|
||||
// section 2 for the latency ladder): both parameters are set by miner signal, and consuming their slots here means a
|
||||
// later use of either changes no other draw. Nothing below reads them, so every value drawn above is what it was.
|
||||
let _epoch_len_draw = s.next();
|
||||
let _latency_ladder_draw = s.next();
|
||||
let mut chosen: Vec<u8> = c[..free].to_vec();
|
||||
chosen.sort_unstable();
|
||||
let mut pos = [0u8; 4];
|
||||
|
|
@ -804,7 +809,43 @@ pub const V3_CLASS: LoadClass = LoadClass { era: None, hot: None, ..LoadClass::M
|
|||
/// of 256 ALU instructions run 27 times per iteration ("mx8+sh256x27", 55,296 shadow instructions per hash). The
|
||||
/// base program, the 16 loads, the item construction, the cache growth rule and the era draw are class v3's, draw
|
||||
/// for draw, so a v4 epoch's day cache and dataset are the v3 day's. Composed with the era exactly as V3 is.
|
||||
pub const V4_CLASS: LoadClass = LoadClass { shadow: Some(ShadowClass { instrs: 256, reps: 27 }), ..V3_CLASS };
|
||||
pub const V4_CLASS: LoadClass = LoadClass { shadow: Some(ShadowClass { instrs: V4_SHADOW_INSTRS, reps: V4_SHADOW_REPS }), ..V3_CLASS };
|
||||
|
||||
/// The shadow block size of class v4 at every rung of the latency ladder (`docs/design/latency-ladder.md`): 256
|
||||
/// instructions. The ladder moves the pass count alone.
|
||||
pub const V4_SHADOW_INSTRS: u16 = 256;
|
||||
|
||||
/// The shadow passes of class v4 at rung 0 of the latency ladder: 27 (`mx8+sh256x27`, about 102,100 counted ops).
|
||||
pub const V4_SHADOW_REPS: u16 = 27;
|
||||
|
||||
/// Class v4 at a rung of the latency ladder (`docs/design/latency-ladder.md` section 2): [`V4_CLASS`] with the
|
||||
/// 256-instruction shadow block run `reps` times per iteration. `reps` 0 means the class's own count, so
|
||||
/// `v4_class_at(0) == v4_class_at(27) == V4_CLASS` and a caller that knows no rung changes nothing.
|
||||
pub fn v4_class_at(reps: u16) -> LoadClass {
|
||||
if reps == 0 {
|
||||
V4_CLASS
|
||||
} else {
|
||||
LoadClass { shadow: Some(ShadowClass { instrs: V4_SHADOW_INSTRS, reps }), ..V3_CLASS }
|
||||
}
|
||||
}
|
||||
|
||||
/// The shadow passes of a class v4 load class at any rung of the ladder, the era draw set aside (`Some(27)` for
|
||||
/// [`V4_CLASS`] itself); `None` for every other class, a measurement class with another block size included.
|
||||
pub fn v4_rung_reps(class: &LoadClass) -> Option<u16> {
|
||||
let base = LoadClass { era: None, ..*class };
|
||||
match base.shadow {
|
||||
Some(ShadowClass { instrs: V4_SHADOW_INSTRS, reps }) if LoadClass { shadow: None, ..base } == V3_CLASS => Some(reps),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Counted integer ops per hash of class v4 at `reps` shadow passes, the convention of
|
||||
/// `docs/analysis/latency-shadow-2026-10-06.md` section 1: 930 for the base program and its loads, 1.83 ops per
|
||||
/// shadow instruction (137 / 75 over the non-load weights), 8 iterations x 256 instructions x `reps` shadow
|
||||
/// instructions. Approximate by construction; the label of a rung, never a consensus value.
|
||||
pub fn v4_counted_ops(reps: u16) -> u64 {
|
||||
930 + (ITERATIONS as u64 * V4_SHADOW_INSTRS as u64 * reps as u64 * 183).div_ceil(100)
|
||||
}
|
||||
|
||||
/// The width set class v3's era draw chooses from: 4 bytes only (the read-width decision of 5 October 2026; the
|
||||
/// draw is consumed, so widening the set at genesis keeps the derivation).
|
||||
|
|
@ -1009,7 +1050,12 @@ impl Program {
|
|||
/// || attempt_le32`. Written into every pack so a version 1 program, or another attempt of the same seed,
|
||||
/// can never be mistaken for this one.
|
||||
pub fn program_id(&self) -> u64 {
|
||||
if self.class.is_v2() || self.generator == GENERATOR_VERSION_V3 || self.generator == GENERATOR_VERSION_V4 {
|
||||
// Latency ladder (docs/design/latency-ladder.md section 7): a class v4 program above rung 0 carries its shadow
|
||||
// size in the id (`program_id_class`, the "shadow/" bytes), so two rungs of one seed never share an id and a
|
||||
// pack of another rung is refused as a pack of another class is. Rung 0 keeps `program_id(4, seed, attempt)`
|
||||
// byte for byte, so every v4 id written before the ladder stands.
|
||||
let v4_rung_0 = self.generator == GENERATOR_VERSION_V4 && LoadClass { era: None, ..self.class } == V4_CLASS;
|
||||
if self.class.is_v2() || self.generator == GENERATOR_VERSION_V3 || v4_rung_0 {
|
||||
// Spec 01 section 1.4.6: a class v3 program's id is `program_id(3, seed, attempt)`, a class v4 program's
|
||||
// `program_id(4, seed, attempt)` (Counter ASIC 3.0); the generator version in the preimage separates
|
||||
// them from every version 2 program of the same seed
|
||||
|
|
@ -1378,6 +1424,27 @@ pub fn generate_from_seed_bytes_program_class(seed_string: &str, seed_bytes: &[u
|
|||
p
|
||||
}
|
||||
|
||||
/// [`generate_from_seed_bytes_program_class`] at a rung of the latency ladder (`docs/design/latency-ladder.md`
|
||||
/// section 2): `shadow_reps` is the shadow pass count the chain's step gives the epoch, 0 for the class's own. Class
|
||||
/// v4 at a rung above 0 draws from [`v4_class_at`] with the era inside and generator 4 stamped; every other class,
|
||||
/// and class v4 at rung 0, is [`generate_from_seed_bytes_program_class`] byte for byte. The base program, the 16 loads
|
||||
/// and the era draw do not move with the rung: only the pass count of the shadow block does.
|
||||
pub fn generate_from_seed_bytes_program_class_shadow(seed_string: &str, seed_bytes: &[u8], class: ProgramClass, era_bytes: Option<&[u8]>, shadow_reps: u16) -> Program {
|
||||
if class != ProgramClass::V4 || shadow_reps == 0 || shadow_reps == V4_SHADOW_REPS {
|
||||
return generate_from_seed_bytes_program_class(seed_string, seed_bytes, class, era_bytes);
|
||||
}
|
||||
let base = v4_class_at(shadow_reps);
|
||||
match era_bytes {
|
||||
Some(era) => generate_era_generator(seed_string, seed_bytes, base, era, &V3_ALLOWED, GENERATOR_VERSION_V4),
|
||||
None => {
|
||||
let mut p = generate_from_seed_bytes_class(seed_string, seed_bytes, base);
|
||||
p.generator = GENERATOR_VERSION_V4;
|
||||
p.era_bytes = None;
|
||||
p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The program of a seed string (its UTF-8 bytes are the program seed).
|
||||
pub fn generate(seed_string: &str) -> Program {
|
||||
generate_from_seed_bytes(seed_string, seed_string.as_bytes())
|
||||
|
|
@ -2027,4 +2094,72 @@ mod tests {
|
|||
assert_eq!(again.shadow, sh.shadow);
|
||||
assert_eq!(crate::verify::hash_warp(&again, 0, &ds), h1);
|
||||
}
|
||||
|
||||
/// Latency ladder (`docs/design/latency-ladder.md`), the known-failed case first: before the ladder a changed N
|
||||
/// was a hard fork. Two nodes drawing class v4 at 27 and at 35 passes from the same seeds build the same base
|
||||
/// program and the same shadow block, carry generator 4 on both and, under the id rule as it stood, the SAME id
|
||||
/// (`program_id(4, seed, attempt)` reads no shadow size), yet their warps hash differently: every block of one is
|
||||
/// invalid to the other and no pack line told them apart. After: the rung is a parameter of the chain's step,
|
||||
/// rung 0 is `V4_CLASS` byte for byte, and a rung above carries its pass count in the id.
|
||||
#[test]
|
||||
fn latency_ladder_known_failed_a_changed_n_was_a_hard_fork_and_rungs_are_class_v4() {
|
||||
let era = [7u8; 32];
|
||||
let today = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era));
|
||||
let r0 = generate_from_seed_bytes_program_class_shadow("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era), 0);
|
||||
let r27 = generate_from_seed_bytes_program_class_shadow("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era), V4_SHADOW_REPS);
|
||||
assert_eq!(r0, today, "rung 0 is class v4 byte for byte");
|
||||
assert_eq!(r27, today, "27 passes is rung 0");
|
||||
let r1 = generate_from_seed_bytes_program_class_shadow("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era), 35);
|
||||
// the failed case, as it stood: the same base, the same block, the same seed words, generator 4 on both, one id
|
||||
assert_eq!(r1.instrs, today.instrs, "the base program is the class's, draw for draw");
|
||||
assert_eq!(r1.shadow, today.shadow, "the block is the same draw; only the pass count moves");
|
||||
assert_eq!((r1.seed, r1.attempt, r1.generator, r1.era_bytes.clone()), (today.seed, today.attempt, today.generator, today.era_bytes.clone()));
|
||||
assert_eq!(program_id(GENERATOR_VERSION_V4, &r1.seed, r1.attempt), program_id(GENERATOR_VERSION_V4, &today.seed, today.attempt), "the old rule gave both nodes one id");
|
||||
assert_eq!(r1.shadow_reps(), 35);
|
||||
assert_eq!(r1.shadow_instrs_per_hash(), ITERATIONS * 256 * 35);
|
||||
let ds = crate::verify::DatasetSource::new("2026-10-06", crate::verify::DatasetMode::ClosedForm, 20);
|
||||
let h0 = crate::verify::hash_warp(&today, 0, &ds);
|
||||
let h1 = crate::verify::hash_warp(&r1, 0, &ds);
|
||||
assert_ne!(h0, h1, "a changed N is another hash: before the ladder, a hard fork");
|
||||
// after: the rung is in the id above rung 0; rung 0 keeps the id written before the ladder
|
||||
assert_ne!(r1.program_id(), today.program_id(), "the rung is in the id");
|
||||
assert_eq!(r1.program_id(), program_id_class(GENERATOR_VERSION_V4, &r1.seed, r1.attempt, &r1.class));
|
||||
assert_eq!(today.program_id(), program_id(GENERATOR_VERSION_V4, &today.seed, today.attempt), "rung 0 keeps the v4 id");
|
||||
assert_eq!(r1.program_class(), ProgramClass::V4, "a rung is class v4");
|
||||
assert_eq!(v4_rung_reps(&r1.class), Some(35));
|
||||
assert_eq!(v4_rung_reps(&today.class), Some(27));
|
||||
assert_eq!(v4_rung_reps(&V4_CLASS), Some(27));
|
||||
assert_eq!(v4_rung_reps(&V3_CLASS), None);
|
||||
assert_eq!(v4_rung_reps(&LoadClass::V2), None);
|
||||
assert_eq!(v4_rung_reps(&LoadClass::MX8.with_shadow(64, 52)), None, "another block size is a measurement class, not a rung");
|
||||
assert_eq!(v4_class_at(0), V4_CLASS);
|
||||
assert_eq!(v4_class_at(27), V4_CLASS);
|
||||
assert_eq!(v4_class_at(35), LoadClass::parse("mx8+sh256x35").unwrap());
|
||||
assert_eq!(LoadClass { era: None, ..r1.class }, v4_class_at(35), "the era rides inside the rung's class");
|
||||
assert_eq!(r1.class.era, today.class.era, "the same era draw at every rung");
|
||||
assert!(check(&r1).is_ok(), "the acceptance rule reads the base program, which did not move");
|
||||
// every rung of the designed ladder is another program with its own id
|
||||
let rungs = [27u16, 35, 53, 88, 173, 267];
|
||||
let ids: Vec<u64> = rungs.iter().map(|&r| generate_from_seed_bytes_program_class_shadow("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era), r).program_id()).collect();
|
||||
for i in 0..ids.len() {
|
||||
for j in 0..i {
|
||||
assert_ne!(ids[i], ids[j], "rungs {} and {} share an id", rungs[i], rungs[j]);
|
||||
}
|
||||
}
|
||||
// the ops labels of the rungs, within 1 percent of the measured rungs of algorithm.md 5.3a
|
||||
for (r, ops) in [(27u16, 102_100u64), (35, 132_100), (53, 199_600), (88, 330_700), (173, 649_400), (267, 1_001_600)] {
|
||||
let got = v4_counted_ops(r);
|
||||
assert!(got.abs_diff(ops) * 100 < ops, "reps {r}: {got} counted ops against the label {ops}");
|
||||
}
|
||||
// other classes ignore the rung; class v4 without an era takes it
|
||||
assert_eq!(generate_from_seed_bytes_program_class_shadow("igneum-genesis", b"igneum-genesis", ProgramClass::V3, Some(&era), 35), generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V3, Some(&era)));
|
||||
assert_eq!(generate_from_seed_bytes_program_class_shadow("igneum-genesis", b"igneum-genesis", ProgramClass::V2, None, 35), generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V2, None));
|
||||
let bare = generate_from_seed_bytes_program_class_shadow("igneum-genesis", b"igneum-genesis", ProgramClass::V4, None, 35);
|
||||
assert_eq!((bare.generator, bare.shadow_reps(), bare.era_bytes.is_none()), (GENERATOR_VERSION_V4, 35, true));
|
||||
// the era stream consumes draws 8 and 9 after the seven it uses, so the seven are what they were: the pinned era
|
||||
// packs of tests/packs.rs hold the values; here, the draw is a function of the bytes and the set alone
|
||||
let e = era_draw(&era, &V3_ALLOWED);
|
||||
assert_eq!(e, era_draw(&era, &V3_ALLOWED));
|
||||
assert_eq!(e.width_words, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ pub mod verify;
|
|||
|
||||
pub use bind::{block_init_words, day_bytes, pow256_from_lane, target64_from_le256};
|
||||
pub use accept::{check as accept_program, AcceptReport, Reject};
|
||||
pub use generator::{generate, generate_from_seed_bytes, generate_from_seed_bytes_program_class, Instr, LoadClass, Op, Program, ProgramClass, GENERATOR_VERSION, GENERATOR_VERSION_V3, GENERATOR_VERSION_V4, V3_CLASS, V4_CLASS};
|
||||
pub use generator::{generate, generate_from_seed_bytes, generate_from_seed_bytes_program_class, generate_from_seed_bytes_program_class_shadow, v4_class_at, v4_counted_ops, v4_rung_reps, Instr, LoadClass, Op, Program, ProgramClass, GENERATOR_VERSION, GENERATOR_VERSION_V3, GENERATOR_VERSION_V4, V3_CLASS, V4_CLASS, V4_SHADOW_INSTRS, V4_SHADOW_REPS};
|
||||
pub use memhard::{cache_log2_words, dataset_log2_words, days_since_genesis, growth_doublings, Cache, MemhardCpu, MixParams, Shape};
|
||||
pub use seed::{fnv1a64, seed_words, SplitMix64};
|
||||
pub use verify::{hash_warp, interpret_warp_init, verify_block, DatasetMode, DatasetSource, Epoch};
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ struct Args {
|
|||
days: u64,
|
||||
/// The program class (Counter ASIC 2.0 seam): v2 (default), v3 (V3_CLASS, generator 3) or v4 (V4_CLASS, generator 4).
|
||||
program_class: Option<ProgramClass>,
|
||||
/// The shadow pass count of a class v4 program at a rung of the latency ladder (`--shadow-reps`, 0 = the class's
|
||||
/// own 27; `docs/design/latency-ladder.md`); read under `--program-class v4` only.
|
||||
shadow_reps: u16,
|
||||
/// The era seed bytes a class v3 chain program records (`--era-hex`).
|
||||
era_hex: Option<String>,
|
||||
/// Era layout: `--era igneum-era-test/<n>` or `--era <n>:<64 hex>` composes the era class over `--class` with
|
||||
|
|
@ -99,6 +102,7 @@ fn usage() -> ! {
|
|||
\x20 also: w4, w16, w64, w64x4, p4,p16,p64[xN], <class>m<mult>[g], <class>+sh<S>x<R> (latency-shadow block of S ALU instructions x R passes per iteration, Counter ASIC 3.0 item 8)\n\
|
||||
\x20 --days N days since genesis for the cache growth rule of a class with it (default 0: the 2^26-word cache)\n\
|
||||
\x20 --program-class v2|v3|v4 the program class of the seam (v3 = generator 3 on V3_CLASS, v4 = generator 4 on V4_CLASS = mx8+sh256x27, the chain's own derivation; --era-hex records the era seed)\n\
|
||||
\x20 --shadow-reps N class v4 at a rung of the latency ladder: the shadow block's pass count (0 = the class's own 27; docs/design/latency-ladder.md), with --program-class v4\n\
|
||||
\x20 --era E era layout over --class: igneum-era-test/<n> or <n>:<64 hex> (the 32-byte era seed E_n)\n\
|
||||
\x20 --era-widths 4[,16,64] the width set the era draws from, in bytes (default 4: pinned; more lets the era draw it)"
|
||||
);
|
||||
|
|
@ -123,6 +127,7 @@ fn parse() -> Args {
|
|||
days: 0,
|
||||
program_class: None,
|
||||
era_hex: None,
|
||||
shadow_reps: 0,
|
||||
era: None,
|
||||
era_widths: vec![1],
|
||||
};
|
||||
|
|
@ -146,6 +151,7 @@ fn parse() -> Args {
|
|||
"--days" => a.days = val().parse().unwrap_or_else(|_| usage()),
|
||||
"--program-class" => a.program_class = Some(ProgramClass::parse(&val()).unwrap_or_else(|| usage())),
|
||||
"--era-hex" => a.era_hex = Some(val()),
|
||||
"--shadow-reps" => a.shadow_reps = val().parse().unwrap_or_else(|_| usage()),
|
||||
"--era" => a.era = Some(parse_era(&val()).unwrap_or_else(|| usage())),
|
||||
"--era-widths" => a.era_widths = parse_widths(&val()).unwrap_or_else(|| usage()),
|
||||
_ => usage(),
|
||||
|
|
@ -222,7 +228,7 @@ fn epoch_of_class(a: &Args, mode: DatasetMode) -> (Epoch, String) {
|
|||
let label = format!("igneum-epoch/{eh}/day/{dh}");
|
||||
let e = match a.program_class {
|
||||
Some(pc) => Epoch {
|
||||
program: Epoch::chain_program(&eb, era.as_deref(), pc, &label),
|
||||
program: Epoch::chain_program_shadow(&eb, era.as_deref(), pc, a.shadow_reps, &label),
|
||||
dataset: Epoch::chain_dataset_day(&db, pc, a.days, a.dataset_log2),
|
||||
},
|
||||
None => Epoch::from_seed_bytes_day(&eb, &db, &label, a.class, a.days, a.dataset_log2),
|
||||
|
|
@ -232,7 +238,7 @@ fn epoch_of_class(a: &Args, mode: DatasetMode) -> (Epoch, String) {
|
|||
_ => {
|
||||
let e = match a.program_class {
|
||||
Some(pc) => {
|
||||
let program = igneum_pow::generator::generate_from_seed_bytes_program_class(&a.seed, a.seed.as_bytes(), pc, era.as_deref());
|
||||
let program = igneum_pow::generator::generate_from_seed_bytes_program_class_shadow(&a.seed, a.seed.as_bytes(), pc, era.as_deref(), a.shadow_reps);
|
||||
let lc = pc.load_class();
|
||||
let shape = Shape::for_class_day(&lc, a.days);
|
||||
let log2 = if lc.growth { igneum_pow::memhard::dataset_log2_words(a.dataset_log2, a.days) } else { a.dataset_log2 };
|
||||
|
|
@ -410,7 +416,7 @@ fn show(a: &Args) {
|
|||
// the load class of --class, the era of --era composed over it
|
||||
let era_hex_bytes = a.era_hex.as_deref().map(|h| igneum_pow::bind::unhex(h).unwrap_or_else(|| usage()));
|
||||
let mut p = match a.program_class {
|
||||
Some(pc) => igneum_pow::generator::generate_from_seed_bytes_program_class(&label, &bytes, pc, era_hex_bytes.as_deref()),
|
||||
Some(pc) => igneum_pow::generator::generate_from_seed_bytes_program_class_shadow(&label, &bytes, pc, era_hex_bytes.as_deref(), a.shadow_reps),
|
||||
None => igneum_pow::generator::generate_from_seed_bytes_class(&label, &bytes, a.class),
|
||||
};
|
||||
if let (None, Some((_, eb, _))) = (a.program_class, &a.era) {
|
||||
|
|
|
|||
|
|
@ -626,6 +626,13 @@ impl Epoch {
|
|||
crate::generator::generate_from_seed_bytes_program_class(label, epoch_seed, class, era_bytes)
|
||||
}
|
||||
|
||||
/// [`Epoch::chain_program`] at a rung of the latency ladder (`docs/design/latency-ladder.md`): `shadow_reps` is
|
||||
/// the shadow pass count the chain's step gives the epoch (0 = the class's own, which is [`Epoch::chain_program`]
|
||||
/// byte for byte). The node's engine and the miner's pack export call this with the step the template names.
|
||||
pub fn chain_program_shadow(epoch_seed: &[u8], era_bytes: Option<&[u8]>, class: ProgramClass, shadow_reps: u16, label: &str) -> Program {
|
||||
crate::generator::generate_from_seed_bytes_program_class_shadow(label, epoch_seed, class, era_bytes, shadow_reps)
|
||||
}
|
||||
|
||||
/// The day's cache and dataset of [`Epoch::from_chain_seeds`], the one entry the node's engine builds a day
|
||||
/// cache through. The class is an argument because the Counter ASIC 2.0 integration gives class v3 its own item
|
||||
/// construction (the mixer multiplier) and cache size schedule (ca2-mixer); today both classes build the day of
|
||||
|
|
|
|||
277
infra/fast-time/latency-ladder.mjs
Normal file
277
infra/fast-time/latency-ladder.mjs
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
#!/usr/bin/env node
|
||||
// The latency ladder's fast-time gate (docs/design/latency-ladder.md section 9; the class-v4-signal.mjs shape): a 3-node
|
||||
// network on override-60x.json, class v4 from genesis (v3 and the v4 floor at 0, the class window 0: class signalling off,
|
||||
// so the ladder opens the header's high byte on its own), the ladder active from DAA 0 with one window of --window DAA
|
||||
// (default 60, one epoch; seven windows = 420 DAA, so the first epoch whose seed block has seven full windows below it is
|
||||
// epoch 8 at DAA 480), each node's ladder signal set by IGNEUM_LADDER_SIGNAL (--signal a,b,c of up|down|none), one real CPU
|
||||
// miner per node. Ports 29720 and up, network igneum-devnet-972, data /tmp/igneum-fast-time-ladder.
|
||||
//
|
||||
// The cases and the known-failed case:
|
||||
// --signal up,up,none --expect no-step two of three miners signal up: about 67 percent, rung 0 must hold (run 10 epochs)
|
||||
// --signal up,up,up --expect step all three: rung 1 (35 shadow passes) from epoch 8, the first with seven full
|
||||
// windows, every miner's rung-1 program id equal to the CLI's --shadow-reps 35 id and
|
||||
// unequal to the rung-0 id, and NO second step inside the next two epochs
|
||||
// --signal up,up,none --expect step the known-failed case: the harness must report FAIL (no step happened)
|
||||
//
|
||||
// node infra/fast-time/latency-ladder.mjs --signal a,b,c --expect step|no-step [--window 60] [--secs 900] [--epochs 10]
|
||||
// IGNEUMD, IGNEUM_MINER, IGNEUM_POW name the binaries (defaults: the ladder fork worktree's target/release and
|
||||
// igneum-pow/target/release/igneum-pow, the layout on igneum-build-1 under /srv/builds/igneum-wt-ladder).
|
||||
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
||||
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
|
||||
import { devAddress } from '../../tools/harness/lib/address.mjs';
|
||||
|
||||
const ROOT = new URL('../../', import.meta.url).pathname;
|
||||
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
|
||||
const BIN = process.env.IGNEUM_LADDER_BIN || `${ROOT}vendor/igneum-node-ladder/target/release`;
|
||||
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
||||
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
||||
const IGNEUM_POW = process.env.IGNEUM_POW || `${ROOT}igneum-pow/target/release/igneum-pow`;
|
||||
const TMP = process.env.IGNEUM_LADDER_TMP || '/tmp/igneum-fast-time-ladder';
|
||||
const BASE = 29720, SUFFIX = 972;
|
||||
const NEVER = '18446744073709551615';
|
||||
const RUNG0 = 27, RUNG1 = 35, WINDOWS = 7, THRESHOLD = 9000;
|
||||
const args = process.argv.slice(2);
|
||||
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? +args[i + 1] : dflt; };
|
||||
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
|
||||
const GENESIS_BITS = flag('genesis-bits', 0x1f010000);
|
||||
const SECS = flag('secs', 900);
|
||||
const EPOCHS = flag('epochs', 10);
|
||||
const WINDOW = flag('window', 60);
|
||||
const SIGNAL = (sflag('signal') || 'up,up,up').split(',').map(s => s.trim().toLowerCase());
|
||||
const EXPECT = sflag('expect') || 'step';
|
||||
if (!['step', 'no-step'].includes(EXPECT) || SIGNAL.length !== 3 || !SIGNAL.every(s => ['up', 'down', 'none'].includes(s))) { console.error('usage: --signal a,b,c (up|down|none) --expect step|no-step'); process.exit(2); }
|
||||
const started = [];
|
||||
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
for (const b of [IGNEUMD, CPU_MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
||||
|
||||
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
||||
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');
|
||||
// the first epoch whose seed block (the last chain block below L*e - lead) can have DAA >= 7 x WINDOW: L*e - lead - 1 >= 7W
|
||||
let FIRST_STEP_EPOCH = 0;
|
||||
while (FIRST_STEP_EPOCH * EPOCH - LEAD - 1 < WINDOWS * WINDOW) FIRST_STEP_EPOCH++;
|
||||
function mergeOverrideText(text, fields) {
|
||||
let out = 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(', ');
|
||||
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
|
||||
}
|
||||
const override = `${TMP}/override.json`;
|
||||
writeFileSync(override, mergeOverrideText(baseText, {
|
||||
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: '0',
|
||||
latency_ladder_activation_daa: '0', latency_ladder_window_daa: String(WINDOW),
|
||||
}));
|
||||
log(`signals ${SIGNAL.join('/')}, expect ${EXPECT}; class v4 from genesis, the ladder active from DAA 0, window ${WINDOW} DAA x ${WINDOWS} (the first epoch that can step is ${FIRST_STEP_EPOCH}, DAA ${FIRST_STEP_EPOCH * EPOCH}); ${EPOCH} DAA per epoch, lead ${LEAD}; run ${SECS} s or ${EPOCHS} epochs`);
|
||||
|
||||
class Node {
|
||||
constructor(i, connect = []) {
|
||||
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
|
||||
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
|
||||
}
|
||||
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
|
||||
async start() {
|
||||
mkdirSync(this.dir, { recursive: true });
|
||||
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex',
|
||||
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`,
|
||||
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
|
||||
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
|
||||
const out = openSync(this.logFile, 'a');
|
||||
// the node's own ladder signal: what its templates carry in bits 15 and 14
|
||||
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_LADDER_SIGNAL: SIGNAL[this.i] } });
|
||||
started.push(this.proc);
|
||||
await sleep(1200);
|
||||
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
|
||||
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}, signals ${SIGNAL[this.i]}`);
|
||||
return this;
|
||||
}
|
||||
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
|
||||
}
|
||||
function miner(bin, argv, name, env = {}) {
|
||||
const out = openSync(`${TMP}/${name}.log`, 'a');
|
||||
const p = spawn(bin, argv, { stdio: ['ignore', out, out], env: { ...process.env, ...env } });
|
||||
started.push(p);
|
||||
return p;
|
||||
}
|
||||
async function stopAll() {
|
||||
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
|
||||
await sleep(1500);
|
||||
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
|
||||
}
|
||||
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
|
||||
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
|
||||
const minerLog = (i) => { try { return readFileSync(`${TMP}/cpu${i}.log`, 'utf8').split('\n'); } catch { return []; } };
|
||||
const STEP_LINE = /Latency ladder step by miner signal: epoch (\d+) moves to rung (\d+) \((\d+) shadow passes, from rung (\d+)\): (up|down) in each of (\d+) consecutive windows of (\d+) DAA .*weakest up (\d+) bps, weakest down (\d+) bps/;
|
||||
const LADDER_LINE = /Latency ladder from the override file/;
|
||||
const ACTIVE_LINE = /Latency ladder active: rungs/;
|
||||
const OWN_LINE = /Latency ladder signal from IGNEUM_LADDER_SIGNAL: this node signals (\w+)/;
|
||||
|
||||
const t0 = Date.now();
|
||||
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
|
||||
const n0 = await new Node(0).start();
|
||||
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start();
|
||||
const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`]).start();
|
||||
const nodes = [n0, n1, n2];
|
||||
for (const n of nodes) log(`n${n.i}: ${n.grepLog(LADDER_LINE).map(l => l.replace(/^.*?(Latency ladder from)/, '$1'))[0] || '(no ladder line)'} | ${n.grepLog(OWN_LINE).map(l => l.replace(/^.*?(this node signals)/, '$1'))[0] || '(no signal line)'}`);
|
||||
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
|
||||
nodes.forEach((n, i) => miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) }));
|
||||
const pay = devAddress('fast-time-ladder');
|
||||
|
||||
const epochs = new Map();
|
||||
let firstStep = null, lastEpoch = -1, lastReport = 0, lastDaa = 0, endAt = null;
|
||||
const samples = [];
|
||||
while (Date.now() - t0 < SECS * 1000) {
|
||||
await sleep(1000);
|
||||
let daa = null, epoch = null, cls = null, reps = null, nextReps = null, step = null, nextStep = null, up = null, upWeak = null, down = null, sig = null, stepEpoch = null, eraSeed = null;
|
||||
try {
|
||||
const t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
|
||||
const pe = t.powEpoch || t.pow_epoch || {};
|
||||
daa = pe.virtualDaaScore ?? t.block?.header?.daaScore; epoch = pe.epochIndex; cls = pe.programClass; eraSeed = pe.eraSeed;
|
||||
reps = pe.latencyLadderReps; nextReps = pe.nextLatencyLadderReps; step = pe.latencyLadderStep; nextStep = pe.nextLatencyLadderStep;
|
||||
up = pe.latencyLadderUpBps; upWeak = pe.latencyLadderUpWeakestBps; down = pe.latencyLadderDownBps; sig = pe.latencyLadderSignal; stepEpoch = pe.latencyLadderStepEpoch;
|
||||
} catch (e) { log(`template: ${e.message}`); }
|
||||
if (epoch != null && epoch !== lastEpoch) {
|
||||
epochs.set(epoch, { class: cls, reps, step, firstSeenDaa: daa, at: +since(), eraSeed: eraSeed == null ? null : String(eraSeed), up_bps: up, up_weakest_bps: upWeak, down_bps: down, step_epoch: stepEpoch ?? null });
|
||||
log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s: template class ${cls} rung ${step} (${reps} passes), next rung ${nextStep} (${nextReps}), up ${up} bps (weakest of ${WINDOWS}: ${upWeak}), down ${down} bps, this node signals ${sig}, step took effect at epoch ${stepEpoch ?? 'none'}`);
|
||||
if (firstStep == null && step > 0) { firstStep = { epoch, daa, step, reps, at: +since() }; log(`LADDER STEP: the template is rung ${step} (${reps} shadow passes) from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
|
||||
lastEpoch = epoch;
|
||||
}
|
||||
lastDaa = daa ?? lastDaa;
|
||||
if (Date.now() - lastReport > 15000) {
|
||||
lastReport = Date.now();
|
||||
const counts = await Promise.all(nodes.map(async n => { try { const d = await n.rpc.call('getBlockDagInfo'); return `${d.blockCount}/${String(d.sink).slice(0, 8)}`; } catch { return '?'; } }));
|
||||
log(`t=${since()} s daa ${daa} epoch ${epoch} rung ${step} (${reps}) up ${up} bps weakest ${upWeak} blocks/sink per node ${counts.join(' ')}`);
|
||||
samples.push({ t: +since(), daa, epoch, step, reps, up_bps: up, up_weakest_bps: upWeak, nodes: counts });
|
||||
}
|
||||
// the end: two epochs after a step (to show no second step), or --epochs epochs when no step is expected
|
||||
if (firstStep != null && daa != null && daa >= (firstStep.epoch + 2) * EPOCH + LEAD) { endAt = +since(); break; }
|
||||
if (firstStep == null && daa != null && daa >= EPOCHS * EPOCH) { endAt = +since(); break; }
|
||||
}
|
||||
await sleep(3000);
|
||||
|
||||
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return { error: e.message }; } }));
|
||||
const genesis = dag[0].pruningPointHash;
|
||||
async function allBlocks(n) {
|
||||
const out = []; let low = genesis; const seen = new Set();
|
||||
for (let round = 0; round < 500; round++) {
|
||||
const r = await n.rpc.call('getBlocks', { lowHash: low, includeBlocks: true, includeTransactions: false });
|
||||
const blocks = r.blocks || [];
|
||||
let added = 0;
|
||||
for (const b of blocks) { const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h); out.push({ hash: h, daa: +b.header.daaScore, version: +b.header.version, chain: !!b.verboseData?.isChainBlock }); added++; }
|
||||
if (!blocks.length || added === 0) break;
|
||||
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let blocks = [];
|
||||
try { blocks = await allBlocks(n0); } catch (e) { log(`getBlocks: ${e.message}`); }
|
||||
const BOUNDARY = firstStep ? firstStep.epoch * EPOCH : Infinity;
|
||||
const before = blocks.filter(b => b.daa < BOUNDARY), after = blocks.filter(b => b.daa >= BOUNDARY);
|
||||
// the ladder bits on the chain: bit 15 up, bit 14 down; the object byte (bits 8 to 13) must be 0 (class signalling off)
|
||||
const bitsOf = (v) => (v & 0x8000) ? 'up' : (v & 0x4000) ? 'down' : 'none';
|
||||
const ladderBits = blocks.reduce((m, b) => { const k = bitsOf(b.version); m[k] = (m[k] || 0) + 1; return m; }, {});
|
||||
const upShareOnChain = blocks.length ? Math.round(10000 * (blocks.filter(b => bitsOf(b.version) === 'up').length) / blocks.length) : 0;
|
||||
const objectBytes = blocks.reduce((m, b) => { const v = (b.version >> 8) & 0x3f; m[v] = (m[v] || 0) + 1; return m; }, {});
|
||||
// genesis carries header version 0 (genesis.rs), every mined block the block version 2 in its low byte
|
||||
const lowBytes = new Set(blocks.filter(b => b.daa > 0).map(b => b.version & 0xff));
|
||||
|
||||
const programs = new Map();
|
||||
for (const i of [0, 1, 2]) for (const l of minerLog(i)) {
|
||||
const m = /epoch seed ([0-9a-f]{64}) day (\d+) \(daa (\d+)\): program and 256 MiB cache ready in ([\d.]+) ms; class (v\d) program id ([0-9a-f]{16})/.exec(l);
|
||||
if (!m) continue;
|
||||
const k = m[1]; const e = programs.get(k) || { seed: k.slice(0, 16), epoch: Math.floor(+m[3] / EPOCH), class: m[5], id: m[6], miners: new Set() };
|
||||
if (e.id !== m[6] || e.class !== m[5]) e.disagree = true;
|
||||
e.miners.add(i); programs.set(k, e);
|
||||
}
|
||||
const programRows = [...programs.values()].sort((a, b) => a.epoch - b.epoch).map(p => ({ epoch: p.epoch, class: p.class, program_id: p.id, seed: p.seed, miners: p.miners.size, disagree: !!p.disagree }));
|
||||
function cliId(seedHex, eraHex, reps) {
|
||||
if (!existsSync(IGNEUM_POW)) return null;
|
||||
const r = spawnSync(IGNEUM_POW, ['show', '--epoch-hex', seedHex, '--program-class', 'v4', '--era-hex', eraHex, '--shadow-reps', String(reps)], { encoding: 'utf8' });
|
||||
const m = /program id ([0-9a-f]{16})/.exec(r.stdout || '');
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
const idRows = [];
|
||||
for (const [k, e] of programs) {
|
||||
const ep = epochs.get(e.epoch);
|
||||
if (!ep || ep.eraSeed == null) continue;
|
||||
const reps = ep.reps ?? 0;
|
||||
idRows.push({ epoch: e.epoch, seed: e.seed, reps, miners_id: e.id, miners: e.miners.size, cli_rung0: cliId(k, ep.eraSeed, 0), cli_at_reps: cliId(k, ep.eraSeed, reps) });
|
||||
}
|
||||
const steppedRows = idRows.filter(r => r.reps !== RUNG0 && r.reps !== 0);
|
||||
const accepted = [0, 1, 2].map(i => minerLog(i).filter(l => /ACCEPTED block/.test(l)).length);
|
||||
const rejectedMiner = [0, 1, 2].map(i => minerLog(i).filter(l => /rejected nonce=|submit error/.test(l)));
|
||||
const rejectedNode = nodes.map(n => n.grepLog(/PoW rejected|Rejected block|rejected block/i));
|
||||
const stepLines = nodes.map(n => n.grepLog(STEP_LINE).map(l => l.replace(/^.*?(Latency ladder step by miner signal)/, '$1')));
|
||||
const firstStepLine = stepLines.map(ls => ls[0] || null);
|
||||
const stepEpochs = firstStepLine.map(l => { const m = l && STEP_LINE.exec(l); return m ? +m[1] : null; });
|
||||
const stepRungs = firstStepLine.map(l => { const m = l && STEP_LINE.exec(l); return m ? +m[2] : null; });
|
||||
const stepWeakestUp = firstStepLine.map(l => { const m = l && STEP_LINE.exec(l); return m ? +m[8] : null; });
|
||||
const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
|
||||
const counts = dag.map(d => d.blockCount ?? '?');
|
||||
const maxEpochSeen = Math.max(-1, ...epochs.keys());
|
||||
const repsSeen = [...epochs.values()].map(e => e.reps);
|
||||
const afterStep = firstStep ? [...epochs.entries()].filter(([e]) => e > firstStep.epoch).map(([, v]) => v.step) : [];
|
||||
|
||||
const common = {
|
||||
zero_rejected_by_miners: rejectedMiner.every(r => r.length === 0),
|
||||
zero_rejected_by_nodes: rejectedNode.every(r => r.length === 0),
|
||||
sinks_agree: new Set(sinks).size === 1,
|
||||
block_counts_agree: new Set(counts.map(String)).size === 1,
|
||||
miners_agree_on_every_program: programRows.every(p => !p.disagree),
|
||||
ladder_line_on_every_node: nodes.every(n => n.grepLog(LADDER_LINE).length > 0 && n.grepLog(ACTIVE_LINE).length > 0),
|
||||
every_node_signals_its_bits: nodes.every((n, i) => n.grepLog(OWN_LINE).some(l => OWN_LINE.exec(l)[1] === SIGNAL[i])),
|
||||
every_epoch_class_v4: [...epochs.values()].every(e => e.class === 4),
|
||||
rung0_ids_equal_the_cli_rung0_id: idRows.filter(r => r.reps === RUNG0).length > 0 && idRows.filter(r => r.reps === RUNG0).every(r => r.cli_rung0 != null && r.cli_rung0 === r.miners_id),
|
||||
};
|
||||
// every block carries block version 2, an object byte of 0 (class signalling off) and the ladder bits of one of the three
|
||||
// nodes; genesis, made before any node, is the one bit-less block when every node signals (the first run of the known-failed
|
||||
// case, 22:26Z, failed this check on genesis's version 0 in the low-byte test, a harness fault, not a chain one)
|
||||
const noneNodes = SIGNAL.filter(s => s === 'none').length;
|
||||
common.chain_carries_the_bits = blocks.length > 0 && [...lowBytes].every(v => v === 2) && Object.keys(objectBytes).every(v => +v === 0)
|
||||
&& Object.keys(ladderBits).every(k => SIGNAL.includes(k) || k === 'none') && (noneNodes > 0 || (ladderBits.none || 0) === 1);
|
||||
let checks;
|
||||
if (EXPECT === 'step') {
|
||||
checks = {
|
||||
...common,
|
||||
template_stepped_to_rung_1: firstStep != null && firstStep.step === 1 && firstStep.reps === RUNG1,
|
||||
stepped_at_the_first_full_window_epoch: firstStep != null && firstStep.epoch === FIRST_STEP_EPOCH,
|
||||
step_line_on_every_node_same_epoch: stepEpochs.every(e => e != null) && new Set(stepEpochs).size === 1 && stepEpochs[0] === (firstStep && firstStep.epoch) && stepRungs.every(r => r === 1),
|
||||
weakest_up_at_or_above_threshold: stepWeakestUp.every(s => s != null && s >= THRESHOLD),
|
||||
no_second_step_inside_seven_windows: firstStep != null && afterStep.length >= 2 && afterStep.every(s => s === 1) && stepLines.every(ls => ls.length === 1),
|
||||
blocks_on_both_sides: before.length > 0 && after.length > 0,
|
||||
rung1_ids_equal_the_cli_rung1_id: steppedRows.length > 0 && steppedRows.every(r => r.reps === RUNG1 && r.cli_at_reps != null && r.cli_at_reps === r.miners_id && r.miners === 3),
|
||||
rung1_ids_differ_from_the_same_seed_rung0_id: steppedRows.length > 0 && steppedRows.every(r => r.cli_rung0 != null && r.cli_rung0 !== r.miners_id),
|
||||
};
|
||||
} else {
|
||||
checks = {
|
||||
...common,
|
||||
template_never_above_rung_0: firstStep == null && repsSeen.every(r => r === RUNG0 || r === 0),
|
||||
no_step_line_on_any_node: stepLines.every(ls => ls.length === 0),
|
||||
ran_the_epochs: maxEpochSeen >= EPOCHS - 1,
|
||||
passed_the_first_full_window_epoch: maxEpochSeen >= FIRST_STEP_EPOCH,
|
||||
up_share_under_threshold_on_chain: upShareOnChain < THRESHOLD,
|
||||
};
|
||||
}
|
||||
const pass = Object.values(checks).every(Boolean);
|
||||
const summary = {
|
||||
pass, expect: EXPECT, signals: SIGNAL, checks, window: WINDOW, windows: WINDOWS, threshold_bps: THRESHOLD, epoch_blocks: EPOCH, lead: LEAD, first_step_epoch: FIRST_STEP_EPOCH,
|
||||
node: IGNEUMD, miner: CPU_MINER, pow: IGNEUM_POW, template_step: firstStep, run_ended_at_s: endAt, final_daa: lastDaa, max_epoch_seen: maxEpochSeen,
|
||||
epochs: Object.fromEntries([...epochs.entries()].map(([k, v]) => [k, v])),
|
||||
blocks: { total: blocks.length, before_boundary: before.length, after_boundary: after.length, ladder_bits: ladderBits, object_bytes: objectBytes, up_share_bps_on_chain: upShareOnChain },
|
||||
programs: programRows, program_id_rows: idRows, accepted_per_miner: accepted,
|
||||
rejected_by_miners: rejectedMiner.map(r => r.length), rejected_by_nodes: rejectedNode.map(r => r.length),
|
||||
sinks, block_counts: counts, step_lines: stepLines, samples,
|
||||
};
|
||||
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
|
||||
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'} (expect ${EXPECT}, signals ${SIGNAL.join('/')}): ${firstStep ? `rung ${firstStep.step} (${firstStep.reps} passes) from epoch ${firstStep.epoch} at DAA ${firstStep.daa}` : 'no step'}; epochs seen ${[...epochs.entries()].map(([e, v]) => `e${e}:r${v.step}:${v.up_weakest_bps}bps`).join(' ')}; chain bits ${JSON.stringify(ladderBits)} (${upShareOnChain} bps up); blocks ${before.length} / ${after.length}; rejected miners ${rejectedMiner.map(r => r.length).join('/')} nodes ${rejectedNode.map(r => r.length).join('/')}; sinks ${sinks.join(' ')} at ${counts.join('/')}`);
|
||||
for (const r of idRows) log(`PROGRAM ID epoch ${r.epoch} seed ${r.seed} reps ${r.reps}: miners ${r.miners_id} (${r.miners} of 3) cli at reps ${r.cli_at_reps} cli rung 0 ${r.cli_rung0}`);
|
||||
for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`);
|
||||
log(`summary: ${TMP}/summary.json`);
|
||||
await stopAll();
|
||||
process.exit(pass ? 0 : 1);
|
||||
|
|
@ -52,14 +52,20 @@
|
|||
"pow_epoch_lead": 10,
|
||||
"pow_day_ms": 1440000,
|
||||
"difficulty_v2_activation_daa": 18446744073709551615,
|
||||
"difficulty_v3_activation_daa": 18446744073709551615,
|
||||
"finality_daa_rule_activation_daa": 18446744073709551615,
|
||||
"proving_v0_activation_daa": 18446744073709551615,
|
||||
"finality_v3_activation_daa": 18446744073709551615,
|
||||
"program_class_v3_activation_daa": 18446744073709551615,
|
||||
"program_class_v4_activation_daa": 18446744073709551615,
|
||||
"program_class_v4_signal_window_daa": 120,
|
||||
"latency_ladder": [{"reps": 27, "admissible": true}, {"reps": 35, "admissible": true}, {"reps": 53, "admissible": true}, {"reps": 88, "admissible": false}, {"reps": 173, "admissible": false}, {"reps": 267, "admissible": false}],
|
||||
"latency_ladder_activation_daa": 18446744073709551615,
|
||||
"latency_ladder_window_daa": 120,
|
||||
"proving_v1_fresh_rule_daa": 18446744073709551615,
|
||||
"exec_restart_number": 18446744073709551615,
|
||||
"exec_restart_hash": "",
|
||||
"exec_restart_state_root": "",
|
||||
"exec_restart_trust_daa": 18446744073709551615,
|
||||
"pow_genesis_dataset_log2": 28,
|
||||
"proving_v1_activation_daa": 18446744073709551615,
|
||||
|
|
|
|||
81
tools/ladder/verify-bench-remote.sh
Executable file
81
tools/ladder/verify-bench-remote.sh
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env bash
|
||||
# The latency ladder's verifier bound, measured on igneum-build-1 (docs/design/latency-ladder.md section 4): for every rung
|
||||
# of the ladder, the cold verify of one 32-lane warp of class v4 at that rung on the reference core (core 40, 3.8 GHz under
|
||||
# schedutil, nice 19), alone and with its SMT sibling (core 88) running the same bench, plus the average of 50 warps. The
|
||||
# figure a rung's admissibility reads is the cold run with the sibling loaded; the gate is 10 ms.
|
||||
#
|
||||
# tools/ladder/verify-bench-remote.sh [--rungs "27 35 53 88 173 267"] [--pow <path on the box>] [--out <dir on the Mac>]
|
||||
#
|
||||
# Runs ON the box under its measure hold (infra/build-server/remote-run.sh BR_MEASURE=1: waits for every running build, blocks
|
||||
# new ones and the capacity layer until it ends; one JSONL line of kind measure in /srv/builds/_log/builds.jsonl). The binary is
|
||||
# the box's own build of igneum-pow from this worktree (tools/build-remote.sh from igneum-pow/ puts it at
|
||||
# /srv/builds/<worktree>/igneum-pow/target/release/igneum-pow); the results come back as one text file per run plus a table.
|
||||
# A number taken beside another build is not a number (CLAUDE.md), which is what the hold is for.
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BS_TOOL=ladder-verify-bench
|
||||
# shellcheck source=../../infra/build-server/lib.sh
|
||||
. "$HERE/../../infra/build-server/lib.sh"
|
||||
RUNGS="27 35 53 88 173 267"; POW=""; OUT=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--rungs) RUNGS="$2"; shift 2 ;; --pow) POW="$2"; shift 2 ;; --out) OUT="$2"; shift 2 ;;
|
||||
*) echo "unknown argument $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
bs_host
|
||||
# the context of the igneum-pow crate (BS_WT, BS_WT_ROOT, branch and sha for the JSONL line); the binary is already on the box
|
||||
( cd "$HERE/../../igneum-pow" ) || bs_die "no igneum-pow beside tools/"
|
||||
pushd "$HERE/../../igneum-pow" > /dev/null; bs_context; popd > /dev/null
|
||||
WT_ROOT="$BS_WT_ROOT"
|
||||
WT_NAME="$BS_WT"
|
||||
POW="${POW:-/srv/builds/$WT_NAME/igneum-pow/target/release/igneum-pow}"
|
||||
OUT="${OUT:-$WT_ROOT/docs/design/latency-ladder-bench}"
|
||||
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
REMOTE_OUT="/srv/builds/_log/ladder-bench/$STAMP"
|
||||
mkdir -p "$OUT"
|
||||
# the command remote-run.sh evals under the measure hold: per rung, the quiet-core run, then the two siblings at once
|
||||
read -r -d '' CMD <<EOF || true
|
||||
set -u; mkdir -p '$REMOTE_OUT'; cd '$REMOTE_OUT'
|
||||
echo "host \$(hostname) load \$(cut -d' ' -f1-3 /proc/loadavg) freq40 \$(cat /sys/devices/system/cpu/cpu40/cpufreq/scaling_cur_freq 2>/dev/null || echo ?) kHz siblings \$(cat /sys/devices/system/cpu/cpu40/topology/thread_siblings_list) pow \$(sha256sum '$POW' | cut -c1-16)" > meta.txt
|
||||
for r in $RUNGS; do
|
||||
nice -n 19 taskset -c 40 '$POW' bench --seed igneum-genesis --day 2026-10-03 --class mx8+sh256x\$r --warps 50 > alone-\$r.txt 2>&1
|
||||
# the sibling's load must outlast the measured run (the first run of this script, 22:06Z: a 50-warp sibling finished during
|
||||
# the measured run's own cache fill, so the "loaded" warps ran alone; 4,000 warps is 20 to 40 s, killed when the run ends)
|
||||
nice -n 19 taskset -c 88 '$POW' bench --seed igneum-genesis --day 2026-10-03 --class mx8+sh256x\$r --warps 4000 > sibling-\$r.txt 2>&1 &
|
||||
sib=\$!
|
||||
sleep 1.5
|
||||
nice -n 19 taskset -c 40 '$POW' bench --seed igneum-genesis --day 2026-10-03 --class mx8+sh256x\$r --warps 50 > loaded-\$r.txt 2>&1
|
||||
kill \$sib 2>/dev/null; wait \$sib 2>/dev/null
|
||||
echo "rung reps=\$r done \$(date -u +%H:%M:%SZ)"
|
||||
done
|
||||
echo "load after \$(cut -d' ' -f1-3 /proc/loadavg)" >> meta.txt
|
||||
EOF
|
||||
bs_log "measuring rungs $RUNGS on $BS_HOST under the measure hold (pow $POW); results to $OUT"
|
||||
BR_MEASURE=1 BR_KIND=measure BR_COMMAND="igneum-pow bench per ladder rung (core 40 alone, then with core 88 loaded)" BR_TARGET=x86_64-unknown-linux-gnu \
|
||||
bs_remote_run "$(dirname "$POW")" "latency ladder verifier bench rungs $RUNGS" "$CMD"
|
||||
bs_rsync -a "$BS_HOST:$REMOTE_OUT/" "$OUT/$STAMP/"
|
||||
# the table: cold = the "warp base 0: single cold run" line, avg = the "CPU verify" line
|
||||
python3 - "$OUT/$STAMP" $RUNGS <<'PY'
|
||||
import re, sys, os
|
||||
d = sys.argv[1]; rungs = sys.argv[2:]
|
||||
def read(name):
|
||||
try: t = open(os.path.join(d, name)).read()
|
||||
except FileNotFoundError: return (None, None, None)
|
||||
cold = re.search(r"warp base 0: single cold run ([\d.]+) ms", t)
|
||||
avg = re.search(r"CPU verify: ([\d.]+) ms per 32-lane warp", t)
|
||||
shadow = re.search(r"(\d+) shadow instructions per hash", t)
|
||||
return (float(cold.group(1)) if cold else None, float(avg.group(1)) if avg else None, int(shadow.group(1)) if shadow else None)
|
||||
out = [open(os.path.join(d, "meta.txt")).read().strip()]
|
||||
out.append("| Rung | reps | Shadow instrs per hash | Counted ops (approx) | Cold, core alone (ms) | Avg of 50, alone (ms) | Cold, sibling loaded (ms) | Avg of 50, sibling loaded (ms) | Under 10 ms loaded |")
|
||||
out.append("|---|---|---|---|---|---|---|---|---|")
|
||||
for i, r in enumerate(rungs):
|
||||
a = read(f"alone-{r}.txt"); l = read(f"loaded-{r}.txt")
|
||||
ops = 930 + (8 * 256 * int(r) * 183 + 99) // 100
|
||||
f = lambda x: "?" if x is None else f"{x:.2f}"
|
||||
ok = "?" if l[0] is None else ("yes" if l[0] < 10.0 else "NO")
|
||||
out.append(f"| {i} | {r} | {a[2] or '?'} | {ops:,} | {f(a[0])} | {f(a[1])} | {f(l[0])} | {f(l[1])} | {ok} |")
|
||||
print("\n".join(out))
|
||||
open(os.path.join(d, "table.md"), "w").write("\n".join(out) + "\n")
|
||||
PY
|
||||
bs_log "done; raw lines in $OUT/$STAMP"
|
||||
Loading…
Reference in a new issue