diff --git a/app/igneum-app/src/ember.rs b/app/igneum-app/src/ember.rs index 9a9f5a354..6cdf4a8c9 100644 --- a/app/igneum-app/src/ember.rs +++ b/app/igneum-app/src/ember.rs @@ -23,8 +23,15 @@ pub const POWER_STEPS_PCT: [u32; 6] = [100, 90, 80, 70, 60, 50]; /// 6 October 2026, run 6: the 5090's best MH/W sat on the 60% floor (1,854 MHz: 0.563 MH/W, the rate within 0.15%), /// so the ladder and the floor go to 45% of the maximum; the 1% rate tolerance is the guard below that pub const CLOCK_STEPS_PCT: [u32; 7] = [100, 90, 80, 70, 60, 50, 45]; -/// A card's clock floor when the vendor reports none: this share of its maximum core clock. -pub const CLOCK_FLOOR_PCT: u32 = 45; +/// A card's clock floor when the vendor reports none: this share of its maximum core clock. 7 October 2026, the PC 1 +/// efficiency passes (docs/bench-log.md): the 5090's best MH per watt sat at 1,200 to 1,300 MHz (39 to 42 percent of +/// 3,090) and the rate fell past 5 percent only at 1,200 (class v3) and 1,100 (class v4), under the old 45 percent +/// floor; so the ladder continues below 45 percent in [`CLOCK_FINE_STEP_MHZ`] steps down to 20 percent of the maximum +/// (618 MHz on the 5090) and the stop rule, not the floor, ends the search. +pub const CLOCK_FLOOR_PCT: u32 = 20; +/// Below the percent ladder's last rung (45 percent) the clock ladder descends in steps of this many MHz until the +/// rate falls more than the tolerance under the cap point's rate (the knee), a step faults, or the floor is reached. +pub const CLOCK_FINE_STEP_MHZ: u32 = 100; /// A point may lose this much rate against the fastest point and still win on MH per watt (the manifest can change it). pub const RATE_TOLERANCE_PCT: f64 = 1.0; /// A step whose hottest GPU reading reaches this is marked hot and cannot win (the engine aborts at 90). @@ -193,6 +200,9 @@ pub struct Plan { pub tolerance_pct: f64, power: Vec, clock_pcts: Vec, + /// the clock ladder below the percent rungs: MHz values from the last rung minus one fine step down to the floor + /// (the core-clock knob of 7 October 2026; empty when the card has no readable maximum clock) + clock_fine: Vec, fixed: Vec, /// Ember 2 (Climb): the start point, the step sizes and the step budget climb: Option, @@ -218,7 +228,7 @@ impl Plan { let mem_step = if limits.mem_max_mhz > limits.mem_default_mhz { ((limits.mem_max_mhz - limits.mem_default_mhz) / 20).max(25) } else { 0 }; let core_step = if limits.clock_max_mhz > 0 { (limits.clock_max_mhz / 20).max(25) } else { 0 }; let start = Point { clock_mhz: limits.clamp_clock(start.clock_mhz), power_pct: start.power_pct.clamp(50, 100), mem_mhz: limits.clamp_mem(start.mem_mhz) }; - Plan { kind: PlanKind::Climb, limits: limits.clone(), before: start, tolerance_pct: goal.tolerance_pct(tolerance_pct), power: Vec::new(), clock_pcts: Vec::new(), fixed: Vec::new(), climb: Some(Climb { start, mem_step, core_step, budget: 5, goal }) } + Plan { kind: PlanKind::Climb, limits: limits.clone(), before: start, tolerance_pct: goal.tolerance_pct(tolerance_pct), power: Vec::new(), clock_pcts: Vec::new(), clock_fine: Vec::new(), fixed: Vec::new(), climb: Some(Climb { start, mem_step, core_step, budget: 5, goal }) } } /// The goal's score of a row: MH per watt for efficiency and balanced, the rate for maximum rate. @@ -285,7 +295,22 @@ impl Plan { } } let clock_pcts = if limits.clock_max_mhz > 0 { CLOCK_STEPS_PCT[1..].to_vec() } else { Vec::new() }; - Plan { kind: PlanKind::Full, limits: limits.clone(), before, tolerance_pct, power, clock_pcts, fixed: Vec::new(), climb: None } + // the fine ladder: from the last percent rung down to the floor in CLOCK_FINE_STEP_MHZ steps (the knob of + // 7 October 2026; the stop rule in `next` ends it at the knee) + let mut clock_fine = Vec::new(); + if limits.clock_max_mhz > 0 { + let last_pct = limits.clamp_clock(limits.clock_max_mhz * CLOCK_STEPS_PCT[CLOCK_STEPS_PCT.len() - 1] / 100); + let floor = limits.clock_floor(); + let mut m = (last_pct / CLOCK_FINE_STEP_MHZ) * CLOCK_FINE_STEP_MHZ; + if m >= last_pct { + m = m.saturating_sub(CLOCK_FINE_STEP_MHZ); + } + while m >= floor && m > 0 { + clock_fine.push(m); + m = m.saturating_sub(CLOCK_FINE_STEP_MHZ); + } + } + Plan { kind: PlanKind::Full, limits: limits.clone(), before, tolerance_pct, power, clock_pcts, clock_fine, fixed: Vec::new(), climb: None } } /// The prior's point, then one neighbour: the next clock step up when the prior caps the clock (is the cap @@ -304,14 +329,14 @@ impl Plan { if neighbour != p { fixed.push(Step { point: neighbour, watts: limits.watts_for(neighbour.power_pct), kind: Kind::Confirm }); } - Plan { kind: PlanKind::Confirm, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), fixed, climb: None } + Plan { kind: PlanKind::Confirm, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), clock_fine: Vec::new(), fixed, climb: None } } /// One step at the card's current point: the before number, and all a measure-only card (Apple, or NVIDIA /// with Power control off) reports. pub fn baseline(limits: &Limits, before: Point, tolerance_pct: f64) -> Plan { let fixed = vec![Step { point: before, watts: limits.watts_for(before.power_pct), kind: Kind::Baseline }]; - Plan { kind: PlanKind::Baseline, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), fixed, climb: None } + Plan { kind: PlanKind::Baseline, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), clock_fine: Vec::new(), fixed, climb: None } } /// How many steps the plan has at most (the clock ladder counts whether or not it runs). @@ -319,7 +344,42 @@ impl Plan { if let Some(c) = &self.climb { return c.budget; } - self.fixed.len() + self.power.len() + self.clock_pcts.len() + self.fixed.len() + self.power.len() + self.clock_pcts.len() + self.clock_fine.len() + } + + /// The cap point's row: the power ladder's choice (the row the clock search is read against), else the first row. + pub fn cap_row(&self, rows: &[Row]) -> Option { + if self.power.is_empty() { + rows.first().cloned() + } else { + choose(&rows[..self.power.len().min(rows.len())], self.tolerance_pct).or_else(|| rows.first().cloned()) + } + } + + /// Why the clock search ended after `rows`, in words, or None while it runs: a faulted clock row (a rejected or + /// mismatched hash during the hold: the fingerprint check), the knee (the rate under the cap point's by more than + /// the tolerance), or the floor. + pub fn clock_stop_reason(&self, rows: &[Row]) -> Option { + let last = rows.last()?; + if last.point.clock_mhz == 0 || !matches!(self.kind, PlanKind::Full) { + return None; + } + let clock_rows = rows.len().saturating_sub(self.power.len()); + if clock_rows == 0 { + return None; + } + if last.mark == Some(Mark::Faulted) { + return Some(format!("fingerprint mismatch at {} MHz, clocks reset", last.point.clock_mhz)); + } + if let Some(cap) = self.cap_row(rows) { + if last.usable() && cap.usable() && cap.mhs > 0.0 && last.mhs < cap.mhs * (1.0 - self.tolerance_pct.max(0.0) / 100.0) { + return Some(format!("rate fell {:.1} percent at {} MHz", 100.0 * (cap.mhs - last.mhs) / cap.mhs, last.point.clock_mhz)); + } + } + if clock_rows >= self.clock_pcts.len() + self.clock_fine.len() { + return Some(format!("the floor at {} MHz", last.point.clock_mhz)); + } + None } pub fn is_empty(&self) -> bool { self.len() == 0 @@ -338,10 +398,22 @@ impl Plan { return Some(self.power[i].clone()); } let k = i - self.power.len(); - let pct = *self.clock_pcts.get(k)?; + // the stop rule (7 October 2026): a faulted clock row or the knee ends the search; the choice is made among + // the rows so far + if k > 0 { + if let Some(reason) = self.clock_stop_reason(rows) { + if !reason.starts_with("the floor") { + return None; + } + } + } + let clock = if k < self.clock_pcts.len() { + self.limits.clamp_clock(self.limits.clock_max_mhz * self.clock_pcts[k] / 100) + } else { + *self.clock_fine.get(k - self.clock_pcts.len())? + }; // the clock ladder rides the power point the power ladder chose (the before point when nothing won) let power_pct = if self.power.is_empty() { self.before.power_pct } else { choose(&rows[..self.power.len()], self.tolerance_pct).map(|r| r.point.power_pct).unwrap_or(self.before.power_pct) }; - let clock = self.limits.clamp_clock(self.limits.clock_max_mhz * pct / 100); // a step whose clamp lands on the previous step's clock is dropped (the floor was reached) if rows.last().map(|r| r.point.clock_mhz == clock).unwrap_or(false) { return None; @@ -921,6 +993,38 @@ pub fn result_line(kind: PlanKind, mhs: f64, watts: f64, eff: f64) -> String { } } +/// The core-clock knob's result on a card (7 October 2026; the UI lane's field shape): the chosen lock against the cap +/// point's unlocked row, and the stop reason in words. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct LockResult { + /// the chosen core clock cap (0 = unlocked) + pub lock_mhz: u32, + pub lock_mhs: f64, + pub lock_w: f64, + pub lock_mhw: f64, + /// the cap point's row (clock 0): the rate and draw the lock is read against + pub unlocked_mhs: f64, + pub unlocked_w: f64, + /// "rate fell 5.1 percent at 1,200 MHz", "fingerprint mismatch at 1,400 MHz, clocks reset", "the floor at 618 MHz", + /// "no lever" (a card without a clock cap), "" while nothing ran + pub lock_note: String, +} + +/// The knob's result from a finished plan's rows and its chosen row. +pub fn lock_result(plan: &Plan, rows: &[Row], chosen: &Row) -> LockResult { + let cap = plan.cap_row(rows); + let (unlocked_mhs, unlocked_w) = cap.as_ref().map(|c| (c.mhs, c.watts)).unwrap_or((0.0, 0.0)); + let ran_clocks = rows.iter().any(|r| r.point.clock_mhz > 0); + let note = if plan.limits.clock_max_mhz == 0 { + "no lever".to_string() + } else if !ran_clocks { + String::new() + } else { + plan.clock_stop_reason(rows).unwrap_or_else(|| format!("stopped at {} MHz", rows.last().map(|r| r.point.clock_mhz).unwrap_or(0))) + }; + LockResult { lock_mhz: chosen.point.clock_mhz, lock_mhs: chosen.mhs, lock_w: chosen.watts, lock_mhw: chosen.eff, unlocked_mhs, unlocked_w, lock_note: note } +} + /// Why a card cannot be tuned beyond measuring, or None when both knobs are available. pub fn control_reason(vendor: &str, limits: &Limits, device: &str, power_control: bool, amd_helper: bool) -> Option { match vendor { @@ -952,7 +1056,9 @@ mod tests { #[test] fn the_full_plan_is_the_power_ladder_then_the_clock_ladder_at_the_chosen_power() { let plan = Plan::full(&l5090(), Point { clock_mhz: 0, power_pct: 80, mem_mhz: 0 }, 1.0); - assert_eq!(plan.len(), 5 + 6, "five power steps (60% and 50% clamp to 400 W; one kept) and six clock steps (90% down to 45%)"); + // five power steps (60% and 50% clamp to 400 W; one kept), six percent rungs (90% down to 45% = 1,390) and the + // fine ladder 1,300 down to the 20% floor (618): 1,300, 1,200, ..., 700 = 7 steps + assert_eq!(plan.len(), 5 + 6 + 7); let first = plan.next(&[]).unwrap(); assert_eq!((first.point, first.watts, first.kind), (Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 575.0, Kind::Power)); // the power ladder: 575, 518, 460, 403, 400 @@ -974,24 +1080,21 @@ mod tests { let s = plan.next(&rows).unwrap(); assert_eq!(s.point.clock_mhz, 2472); rows.push(row_at(s.point, 220.0, 123.5)); - rows.push(row_at(plan.next(&rows).unwrap().point, 200.0, 118.0)); let s = plan.next(&rows).unwrap(); - assert_eq!(s.point.clock_mhz, 1854, "60% of 3,090"); - rows.push(row_at(s.point, 180.0, 100.0)); - let s = plan.next(&rows).unwrap(); - assert_eq!(s.point.clock_mhz, 1545, "50%"); - rows.push(row_at(s.point, 170.0, 90.0)); - let s = plan.next(&rows).unwrap(); - assert_eq!(s.point.clock_mhz, 1390, "45% of 3,090 is the floor (6 October 2026)"); - rows.push(row_at(s.point, 160.0, 80.0)); + assert_eq!(s.point.clock_mhz, 2163, "70%"); + rows.push(row_at(s.point, 200.0, 118.0)); + // the stop rule (7 October 2026): 118 is 4.8% under the cap point's 124, past the 1% tolerance, so the search + // ends here (the 6 October ladder went on to 1,854, 1,545 and 1,390) assert_eq!(plan.next(&rows), None); + assert_eq!(plan.clock_stop_reason(&rows).as_deref(), Some("rate fell 4.8 percent at 2163 MHz")); // the choice: 2,472 MHz keeps 99.6% of the top rate at 220 W = 0.561 MH/W; 2,163 MHz (118 MH/s) is outside the 1% tolerance let best = choose(&rows, 1.0).unwrap(); assert_eq!(best.point, Point { clock_mhz: 2472, power_pct: 100, mem_mhz: 0 }); // a wider tolerance lets the 2,163 MHz step (0.590 MH/W, 4.8% slower) win assert_eq!(choose(&rows, 5.0).unwrap().point.clock_mhz, 2163); // no power limits, clocks only; no clocks, power only; nothing, empty - assert_eq!(Plan::full(&Limits { clock_max_mhz: 2000, ..Default::default() }, Point::default(), 1.0).len(), 6); + // clocks only: six percent rungs (1,800 .. 900) then the fine ladder 800 .. 400 (the 20% floor) = 5 more + assert_eq!(Plan::full(&Limits { clock_max_mhz: 2000, ..Default::default() }, Point::default(), 1.0).len(), 6 + 5); assert_eq!(Plan::full(&Limits { power_default_w: 300.0, ..Default::default() }, Point::default(), 1.0).len(), 6); assert!(Plan::full(&Limits::default(), Point::default(), 1.0).is_empty()); } @@ -1017,11 +1120,12 @@ mod tests { #[test] fn limits_never_exceed_the_vendor_or_undercut_the_floor() { let l = l5090(); - assert_eq!(l.clock_floor(), 1390); - assert_eq!(l.clamp_clock(1000), 1390); + assert_eq!(l.clock_floor(), 618, "20% of 3,090 (7 October 2026; the 45% floor of 6 October sat on the 5090's knee)"); + assert_eq!(l.clamp_clock(1000), 1000); + assert_eq!(l.clamp_clock(500), 618); assert_eq!(l.clamp_clock(5000), 3090); assert_eq!(l.clamp_clock(0), 0, "unlocked stays unlocked"); - assert_eq!(Limits { clock_max_mhz: 3000, clock_min_mhz: 2100, ..Default::default() }.clamp_clock(1500), 2100, "the vendor's floor wins over the 45% rule"); + assert_eq!(Limits { clock_max_mhz: 3000, clock_min_mhz: 2100, ..Default::default() }.clamp_clock(1500), 2100, "the vendor's floor wins over the 20% rule"); assert_eq!(l.watts_for(100), 575.0); assert_eq!(l.watts_for(50), 400.0); assert_eq!(Limits { power_default_w: 300.0, power_max_w: 250.0, ..Default::default() }.watts_for(100), 250.0); @@ -1321,4 +1425,138 @@ mod tests { assert!(control_reason("amd", &Limits::default(), "1", false, true).is_none()); } } + + /// The core-clock knob (7 October 2026, the PC 1 efficiency passes): a flat ladder walks below the old 45 percent + /// floor in 100 MHz steps to the 20 percent floor, and the result names the floor. + #[test] + fn the_clock_ladder_continues_below_45_percent_in_100_mhz_steps_to_the_floor() { + let plan = Plan::full(&l5090(), Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 1.0); + let mut rows = Vec::new(); + for _ in 0..5 { + let s = plan.next(&rows).unwrap(); + rows.push(row_at(s.point, 300.0, 136.8)); + } + let mut clocks = Vec::new(); + while let Some(s) = plan.next(&rows) { + assert_eq!(s.kind, Kind::Clock); + clocks.push(s.point.clock_mhz); + // the rate holds (memory-bound): the draw falls with the clock + rows.push(row_at(s.point, 300.0 - clocks.len() as f64 * 10.0, 136.0)); + } + assert_eq!(clocks, vec![2781, 2472, 2163, 1854, 1545, 1390, 1300, 1200, 1100, 1000, 900, 800, 700]); + assert_eq!(plan.clock_stop_reason(&rows).as_deref(), Some("the floor at 700 MHz")); + let chosen = choose(&rows, 1.0).unwrap(); + assert_eq!(chosen.point.clock_mhz, 700, "flat rate: the lowest draw wins"); + let r = lock_result(&plan, &rows, &chosen); + assert_eq!((r.lock_mhz, r.lock_w, r.unlocked_mhs, r.unlocked_w), (700, 170.0, 136.8, 300.0)); + assert_eq!(r.lock_note, "the floor at 700 MHz"); + } + + /// The stop rule on PC 1's RTX 5090 rows of 7 October 2026 (class v3, the card alone): the first clock row more + /// than the tolerance under the cap point's rate ends the search and the best MH per watt among the rows within + /// tolerance is chosen. At the 1 percent tolerance the 5090's rate (136.6 at 2,781) is 1.24 percent down at + /// 1,545 MHz, so the search ends there and 1,854 MHz (135.6 MH/s at 239.6 W) is the point; at 1.5 percent it runs + /// on to 1,200 (5.2 percent down) and 1,300 MHz is the point. The tolerance is the manifest's. + #[test] + fn the_clock_search_stops_at_the_knee_and_names_it() { + let measured: Vec<(u32, f64, f64)> = vec![(2781, 317.9, 136.6), (2472, 276.0, 136.4), (2163, 252.4, 136.1), (1854, 239.6, 135.6), (1545, 232.1, 134.9), (1390, 229.0, 134.85), (1300, 223.3, 134.6), (1200, 215.7, 129.5)]; + let walk = |tolerance: f64| -> (Plan, Vec) { + let plan = Plan::full(&Limits { clock_max_mhz: 3090, ..Default::default() }, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, tolerance); + let mut rows = vec![]; + for (mhz, w, mhs) in &measured { + let Some(s) = plan.next(&rows) else { break }; + assert_eq!(s.point.clock_mhz, *mhz); + rows.push(row_at(s.point, *w, *mhs)); + } + (plan, rows) + }; + let (plan, rows) = walk(1.0); + assert_eq!(rows.last().unwrap().point.clock_mhz, 1545, "the search ends on the first row over 1 percent under the cap row"); + assert_eq!(plan.next(&rows), None); + let reason = plan.clock_stop_reason(&rows).unwrap(); + assert!(reason.starts_with("rate fell 1.2 percent at 1545 MHz"), "{reason}"); + let chosen = choose(&rows, 1.0).unwrap(); + assert_eq!(chosen.point.clock_mhz, 1854, "the best MH per watt within 1 percent of the fastest row"); + let r = lock_result(&plan, &rows, &chosen); + assert_eq!((r.lock_mhz, r.unlocked_mhs), (1854, 136.6)); + assert!((r.lock_mhw - 135.6 / 239.6).abs() < 1e-6); + let (plan, rows) = walk(1.5); + assert_eq!(rows.last().unwrap().point.clock_mhz, 1200); + assert_eq!(plan.next(&rows), None); + assert!(plan.clock_stop_reason(&rows).unwrap().starts_with("rate fell 5.2 percent at 1200 MHz")); + assert_eq!(choose(&rows, 1.5).unwrap().point.clock_mhz, 1300); + } + + /// The fingerprint rule: a clock row marked Faulted (a rejected or mismatched hash during the hold) ends the search + /// at once; the choice is made among the usable rows and the note says why. + #[test] + fn a_faulted_clock_row_ends_the_search_and_the_note_says_so() { + let plan = Plan::full(&Limits { clock_max_mhz: 3090, ..Default::default() }, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 1.0); + let mut rows = vec![]; + for (mhz, w, mhs) in [(2781, 317.9, 136.6), (2472, 276.0, 136.4)] { + let s = plan.next(&rows).unwrap(); + assert_eq!(s.point.clock_mhz, mhz); + rows.push(row_at(s.point, w, mhs)); + } + let s = plan.next(&rows).unwrap(); + assert_eq!(s.point.clock_mhz, 2163); + let mut bad = row_at(s.point, 252.4, 136.1); + bad.faults = 1; + bad.mark = Some(Mark::Faulted); + rows.push(bad); + assert_eq!(plan.next(&rows), None, "the search ends on the faulted row"); + assert_eq!(plan.clock_stop_reason(&rows).as_deref(), Some("fingerprint mismatch at 2163 MHz, clocks reset")); + let chosen = choose(&rows, 1.0).unwrap(); + assert_eq!(chosen.point.clock_mhz, 2472, "the faulted row never wins"); + assert_eq!(lock_result(&plan, &rows, &chosen).lock_note, "fingerprint mismatch at 2163 MHz, clocks reset"); + } + + /// The same through the state machine with a fake helper (the known-failed case first: a mismatch mid-search must + /// reset and abort): the run applies 2,781 and 2,472, a fault lands during 2,163's hold, the row comes out Faulted, + /// the next step is none, and the run's final Apply is the chosen 2,472 point (the reset), then Finished. + #[test] + fn a_mismatch_mid_search_resets_to_the_chosen_point_and_finishes() { + let plan = Plan::full(&Limits { clock_max_mhz: 3090, ..Default::default() }, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 1.0); + let timing = Timing { settle: Duration::from_secs(1), hold: Duration::from_secs(2), apply: Duration::from_secs(3) }; + let t0 = Instant::now(); + let mut run = Run::new(0, "0", "card-0", plan, 300.0, false, timing, t0); + let mut t = t0; + let mut applied: Vec = Vec::new(); + let mut finished: Option = None; + let watts_for = |mhz: u32| -> f64 { match mhz { 2781 => 317.9, 2472 => 276.0, _ => 252.4 } }; + for _ in 0..200 { + t += Duration::from_millis(500); + let acked = true; + let limit = run.current.as_ref().map(|s| s.watts).unwrap_or(0.0); + // the fake helper: every setting takes; during 2,163's hold the worker reports a mismatched hash + if let Some(cur) = run.current.clone() { + if matches!(run.phase, Phase::Holding { .. }) { + run.sample_rate(136.4); + run.sample_telemetry(watts_for(cur.point.clock_mhz), cur.point.clock_mhz as f64, 13801.0, 60.0); + if cur.point.clock_mhz == 2163 { + run.sample_fault(); + } + } + } + for o in run.tick(t, Readback { limit_w: limit, acked }) { + match o { + Out::Apply(s) => applied.push(s), + Out::Finished(r) => finished = Some(r), + Out::Failed(e) => panic!("the run failed: {e}"), + Out::Row(_) => {} + } + } + if finished.is_some() { + break; + } + } + let clocks: Vec = applied.iter().map(|s| s.point.clock_mhz).collect(); + assert_eq!(clocks, vec![2781, 2472, 2163, 2472], "2,781, 2,472, the faulted 2,163, then the reset to the chosen 2,472"); + assert_eq!(applied.last().unwrap().kind, Kind::Confirm); + let f = finished.expect("finished"); + assert_eq!(f.point.clock_mhz, 2472); + assert_eq!(run.rows.len(), 3); + assert_eq!(run.rows[2].mark, Some(Mark::Faulted)); + assert_eq!(run.plan.clock_stop_reason(&run.rows).as_deref(), Some("fingerprint mismatch at 2163 MHz, clocks reset")); + } } diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index d111cd9e6..8f0a15db9 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -2727,6 +2727,13 @@ impl Engine { cc.tune_steps = of; cc.tune_eta_s = eta; cc.tune_plan = plan.into(); + // the clock search's own step count while a clock step runs (the UI's "locking clocks: step 4 of 9") + if let Some(r) = self.sweep.as_ref() { + let power_steps = r.rows.iter().filter(|x| x.point.clock_mhz == 0 && x.mark.is_some()).count() as u32; + let on_clock = r.current.as_ref().map(|s| s.kind == crate::ember::Kind::Clock).unwrap_or(false); + cc.lock_step = if on_clock { step.saturating_sub(power_steps) } else { 0 }; + cc.lock_steps = if on_clock { of.saturating_sub(power_steps) } else { 0 }; + } } if self.shared.runtime.sweep_only { // the job playbook forwards this to the installed app's /api/tune-progress @@ -2765,6 +2772,22 @@ impl Engine { c.tune_source = kind.name().into(); c.tune_line = crate::ember::result_line(kind, row.mhs, row.watts, row.eff); c.tune_curve = run.rows.iter().map(|r| r.json()).collect(); + // the core-clock knob's result (7 October 2026): the chosen lock against the cap point, and why it stopped + if kind == crate::ember::PlanKind::Baseline { + c.lock_note = if c.vendor == "apple" || run.plan.limits.clock_max_mhz == 0 { "no lever".into() } else { c.sweep_note.clone() }; + } else { + let lr = crate::ember::lock_result(&run.plan, &run.rows, &row); + c.lock_mhz = lr.lock_mhz; + c.lock_mhs = lr.lock_mhs; + c.lock_w = lr.lock_w; + c.lock_mhw = lr.lock_mhw; + c.unlocked_mhs = lr.unlocked_mhs; + c.unlocked_w = lr.unlocked_w; + c.lock_at = unix as f64; + c.lock_note = lr.lock_note; + } + c.lock_step = 0; + c.lock_steps = 0; let control = c.tune_control; if kind == crate::ember::PlanKind::Baseline { c.sweep_note = if control { String::new() } else { c.sweep_note.clone() }; diff --git a/app/igneum-app/src/state.rs b/app/igneum-app/src/state.rs index d4d510976..67b61c9e7 100644 --- a/app/igneum-app/src/state.rs +++ b/app/igneum-app/src/state.rs @@ -135,6 +135,18 @@ pub struct CardState { // Ember 2: the memory clock the last tune chose and the measured curve (every row of the last plan) pub tune_mem_mhz: u32, pub tune_curve: Vec, + // the core-clock knob (7 October 2026, src/ember.rs lock_result; the UI lane's field shape): the chosen lock + // against the cap point's unlocked row, the step while the clock search runs, the moment and the stop reason + pub lock_mhz: u32, // the chosen core clock cap (0 = unlocked) + pub lock_mhs: f64, + pub lock_w: f64, + pub lock_mhw: f64, + pub unlocked_mhs: f64, // the cap point's row the lock is read against + pub unlocked_w: f64, + pub lock_step: u32, // the clock search's step while it runs (0 otherwise) + pub lock_steps: u32, + pub lock_at: f64, // unix s the lock point was taken (0 = never) + pub lock_note: String, // "rate fell 5.1 percent at 1200 MHz", "fingerprint mismatch at 1400 MHz, clocks reset", "the floor at 700 MHz", "no lever" // the kernel variant race (docs/design/miner-tuning.md): what the worker's last race chose pub variant: String, pub race_mhs: f64, diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index ca3002af7..09d7d35df 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -2497,6 +2497,10 @@ The second sub-version 3 commit is 017e70376489251e18564c0abce7e466e606c8b3 (pus F8's 64-seed gate on 017e7037 (the attack-pass lane, box 2, last seed 16:00:20 UTC): 60 of 64 under 1.2x; the four over are the named tail and nothing else (p10 1.5036x, p8 1.3776x, p34 1.2505x, p4 1.2167x; hottest items 355 to 541 reads of 2^31, unattributed, inside the ratio's clean spread); p23, p19, p15, p18 and p56 under the line; the clean spread 0.9915x to 1.144x. Exhaustion on 017e7037: 0 in the attack-pass lane's 20,532 chain-shaped seeds (max attempt 29) plus this lane's 4,099 (max 17); the 10^6 count continues as a strengthening line. Cost line for the node: the chain's epoch draw now pays the 2^20 ratio pass on every candidate that reaches it (the accepted one, and the about 4 percent that fail there), 2.2 s per pass on one box-2 core, so about 2.3 s per epoch draw on that core and, by the attack-pass lane's reading, about 4 to 5 s per epoch per node on slower cores; the earlier rejections cost milliseconds. From the attack-pass lane sub-version 3 at 017e7037 reads green on both gates with the named tail; its close line went to the plan, main and the Counter ASIC lane. This row stays open on the tail (p4, p8, p10, p34) until it is attributed or ruled accepted. +Two in-house pass findings against sub-version 3 (adv-accept-3, 7 October 2026, evening), neither a chain consequence. (1) Documentation: program.json's `program_id_derivation` and spec 01 section 1.4.6 stated the plain form while generator 4 appends `'sub/' || sub_version_le16`, so a client written from the text derives another id (the devnet v4 pack: 8aa9f185d63f269e from the text against a785001687d8688a; Devnet 3's epoch 0: 30956569d8f3d8d7 against fce15bf61030be57). Fixed at 15008aca and 0f45c8be: one byte recipe (`generator::IdRecipe`) builds the id and the text, the emitter prints that text, the spec states the suffix, the class v5 form (generator 5, no suffix) and the rung form, `tests/derivation.rs` re-derives every pinned pack's id from its own text with the pack's fields only (the plain text as the known-failed case), 38 packs' program.json corrected one line each with every other file and every id and fingerprint byte-identical on re-export; the twelve readwidth mix packs of 5 October keep their text (a class name the parser no longer accepts). (2) Correctness, unreachable: the last-resort program after 256 failed attempts (`last_resort_v4`, the xor rewrite) fails the acceptance rule in 223 of 2,500 seeds (209 by part (a)) and is handed to the chain unchecked; at r = 0.67 to 0.68 the path is reached with probability about 4.6e-44 per epoch, so no chain reaches it and sub-version 3 is not reopened for it. Sub-version 3's last resort is therefore UNREACHABLE AND UNVERIFIED against the rule; class v5's generator (the v5 lane) carries the verified construction with the known-failed test on one of the 223 seeds, and its entry states it. From the same pass: the three lowest-ratio steering seeds of 4,975 at a 256-unit sample read 1.0002x worst live, so the 256-unit ratio is noise as a selector and the selector works only at the 2^20-unit read, which is what rule (c'') uses. + +Owed (recorded, not run, by the founder's word): G2 (the CPU verifier on 1,024 hashes per card) on the amended stream; G3 (the Metal fuzz, edge, stats and determinism runs) on the amended stream; the hash-rate ladder re-measure on the M5 Max and the RTX 5090 (the amendment changes the base program's source draws, not the op mix or the load count, so the latency-bound rows of `docs/analysis/latency-shadow-2026-10-06.md` are expected to hold within their spread; unmeasured); AMD (the RX 9070 XT, PC 1); the 2019-class verifier core (O-1.14); F8's phase E (the 64-seed dynamic census) on the amended stream, which is the attack-pass lane's and the test of the per-op table. The row reads FIXED-AND-PASSED only after phase E passes against the amended class. + Owed (recorded, not run, by the founder's word): G2 (the CPU verifier on 1,024 hashes per card) on the amended stream; G3 (the Metal fuzz, edge, stats and determinism runs) on the amended stream; the hash-rate ladder re-measure on the M5 Max and the RTX 5090 (the amendment changes the base program's source draws, not the op mix or the load count, so the latency-bound rows of `docs/analysis/latency-shadow-2026-10-06.md` are expected to hold within their spread; unmeasured); AMD (the RX 9070 XT, PC 1); the 2019-class verifier core (O-1.14); F8's phase E (the 64-seed dynamic census) on the amended stream, which is the attack-pass lane's and the test of the per-op table. The row reads FIXED-AND-PASSED only after phase E passes against the amended class. Status: Fixed in part, finding bounded, stated (7 October 2026, night, the Counter ASIC lane's words): class v4 sub-version 3 (igneum-pow 017e7037, the audit-freeze tag) is frozen with the dataflow rule, the shared-operand rule, the 0.98 ratio and the total draw; the in-house pass's F8 re-gate reads 60 of 64 seeds under 1.2x with the four-seed tail accepted by the coordinator as the window model's unattributed residue (no chip consequence); the pass then attributed the class by value (eight live hot sets at 1.54x to 2.24x in the lowest 30 of 29,032 accepted programs, each about 1 MB of items at 0.3 percent of reads, 1.002x to a chip); class v5 (1c420786, frozen 21:53 UK) carries the fix as rule (c'''), the per-site distinct-index floor at 0.995 on the state flag (its census refuses 2.435 percent of accepted programs; seven of seven live hot sets refused at 0.9821 to 0.9919; the eighth's ratio owed tonight), with a named residual (three mild shadow-block-written concentrations at 0.9992 to 0.9997, about 1.0004x, a value-level test in the next class); the record is `docs/plans/counter-asic-3-status.md` section 7c and the class v5 design's section 14. The eighth live hot set (seed 122960, id 4be7393ab6c84802, the deepest found: X_f +0.111 percent, 1.54x the window model, its hottest item at 475,616 reads from an all-ones source) reads minimum site 12 at 0.9824 at the acceptance's own 2^20 sample (live 0.9822), refused by class v5's (c''') floor at 0.995; so the floor refuses eight of eight live hot sets by X_f at or above f found in the tail of 88,051 accepted programs (minimum sites 0.9821 to 0.9919) against 0 hot sets in 20 random programs; what it misses stays the three mild shadow-block-written concentrations at 0.9992 to 0.9997 (Devnet 3's first program among them), about 1.0004x to a chip, the value-level test in the next class (22:41 BST; the logs under `docs/analysis/cryptanalysis/logs/adv-accept/` on branch adv-accept; the v5 design's section 14). The RTX 5080 grid's knee is not in tonight; X37 keeps the 5080's stock premium only. @@ -2505,6 +2509,14 @@ Status: Fixed in part, finding bounded, stated (7 October 2026, night, the Count The three genesis fields of `docs/analysis/mission/mission.md` section 2.8, built on the node fork branch `genesis-forward` (from release-0.3.19-node dc141409) and the repo branch `genesis-forward`; the design and the gates in `docs/design/genesis-forward.md`. Every switch is never on the devnet (its digest c562d70e... does not move); the testnet genesis sets all three (the testnet lane re-pins and re-digests). +### AP-F8-4. The program id's derivation text omitted generator 4's sub-version suffix (interoperability, documentation; no object change) + +Found by the in-house pass adv-accept-3 (report-acceptance-rule-3.md at 0c150e3c, finding 2, 7 October 2026, evening): program.json's `program_id_derivation` and spec 01 section 1.4.6 stated `FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words || attempt_le32` while the code (generator.rs `program_id`) appends `'sub/' || sub_version_le16` under generator 4, so an implementation written from the text derives another id (the devnet v4 pack: 8aa9f185d63f269e from the text against the pack's and the chain's a785001687d8688a; Devnet 3's epoch 0: 30956569d8f3d8d7 against fce15bf61030be57); a second client written from the text would disagree with the chain on every program id. The code is the consensus; the text was wrong. + +Fix (15008aca and 0f45c8be on ca3-v4-amend, landed on master through the gate on 7 October 2026 night): the id and its printed derivation come from one byte recipe (`generator::IdRecipe`, `program_id_recipe`, `program_id_class_recipe`, `Program::program_id_derivation`), so the two cannot drift; the emitter prints that text; spec 1.4.6 states the suffix, the class v5 form (generator 5, no suffix) and the class-rung form (`'igneum-program-rw/'` with the class fields); `igneum-pow/tests/derivation.rs` re-derives each of the 18 pinned packs' ids from its own derivation string with the pack's fields only (`every_pinned_pack_id_rederives_from_its_own_derivation_text`; the plain text as the known-failed case in `the_class_v4_derivation_states_the_suffix_and_the_plain_text_derives_another_id`), and the record maker `print_every_pack_derivation` (ignored) rebuilds every pack to set its line. 38 packs' program.json corrected by one line each; every kernel, header, vector and id byte-identical on re-export (the twelve 5 October readwidth mix packs keep their text, their class name no longer parses). igneum-pow against the frozen 017e7037 differs in emit.rs (the one print), generator.rs (the recipe refactor; the hashed bytes unchanged, every pinned id and the recheck pins hold) and the new test only; no acceptance or draw code moved. program.json is OUTSIDE the digested pack bytes: the miner's `kernel_sha256` covers kernel.cu, kernel_bound.cu, kernel.cl, kernel_bound.cl, program.h and memhard.h, and program.json carries that digest as a field; the chain pairs on the program id and the kernel text, never on program.json, so the correction rides any line. The spec read-back check (an implementation from the text reproduces the shipped constants and ids) is the site audit lane's tools/ci/spec-constants-check.mjs on spec-accept-23, written to the hash lane's file-and-line map of 017e7037; finding 3 (the spec does not describe the shipped acceptance rule) is that lane's text in the same landing. + +Status: Fixed (7 October 2026, night): the id and its derivation text come from one byte recipe, every pinned pack's id re-derives from its own text in the suite (the igneum-pow suite on box 2 green at 0f45c8be: 64 + 2 + 7 + 4 + 19 + 2 + 7), the spec states the suffix; no object moved. + ### GF1. A post-quantum signature scheme would need a hard fork, and every vote key is a public BLS12-381 point "When a quantum computer comes, every BLS vote key is forged and the chain has no way to change the scheme without a fork of the kind you say you never need." diff --git a/docs/ledger-public.md b/docs/ledger-public.md index 2cdcaa68e..fc004eb61 100644 --- a/docs/ledger-public.md +++ b/docs/ledger-public.md @@ -2,7 +2,7 @@ Generated by `tools/ledger/export-public.mjs` from `docs/fud-ledger.md`; a gate check fails when the two drift. One row per item: the claim or criticism, its status, what was done, and the evidence. Internal identifiers, times of day and team-member names are left out on purpose; the full ledger is published with the repository. -191 items. By status: Conceded, stated 52; Fixed 30; Decided 22; Fixed on a branch, pending merge 14; Answered by design 9; Answered with evidence 6; Fixed, stated 4; Closed by rule 3; Open 3; Answered by design, with a correction to our own text 1; Answered with evidence, stated 1; Answered by design for finality, Conceded for the lottery 1; Conceded, implemented, stated 1; Rule implemented and measured; launch month simulated 1; Answered by design, with the concession stated 1; Conceded, stated in the litepaper and the design doc 1; Answered with evidence at 1 block/s 1; Conceded, stated in the simulation report 1; Answered by design, with the dependency conceded. Update 7… 1; Conceded, stated in the litepaper, with the dial explained 1; Closed by spec 1; Conceded by decision, stated in the design doc 1; Answered by design, with the founder's edge conceded 1; Answered by design, with a metrics caveat 1; Closed by removal, 3 October 2026 1; Conceded, stated in the litepaper 1; Conceded, stated in the design doc 1; Measured on the live node line, and the overlay does NOT… 1; Conceded, stated in the simulation 1; Conceded in part, labelled, stated 1; Fixed in the node 1; Answered with evidence for the largest body the rules allow 1; Spec fixed 1; Fixed in the proving code 1; Fixed in the spec 1; Rule fixed 1; Rule written 1; Fixed, logged 1; Answered with evidence for the test half 1; Fixed in the node and shipped, rule not yet activated on… 1; Simulation half run 1; Answered with evidence for all four 1; Written 1; Designed 1; Fixed and confirmed 1; Rolled out 1; Conceded by decision 1; Conceded, scheduled, stated 1; Conceded, contained by rule, stated 1; Fixed on a branch and verified locally 1; Answered with evidence and stated 1; Answered with evidence for PC 2 1; Answered by design and with evidence 1; Fixed, stated; restated 1; Fixed, stated; restated further 1; Fixed in part, finding bounded, stated 1; Fixed as a genesis lever, measurement owed 1. +192 items. By status: Conceded, stated 52; Fixed 31; Decided 22; Fixed on a branch, pending merge 14; Answered by design 9; Answered with evidence 6; Fixed, stated 4; Closed by rule 3; Open 3; Answered by design, with a correction to our own text 1; Answered with evidence, stated 1; Answered by design for finality, Conceded for the lottery 1; Conceded, implemented, stated 1; Rule implemented and measured; launch month simulated 1; Answered by design, with the concession stated 1; Conceded, stated in the litepaper and the design doc 1; Answered with evidence at 1 block/s 1; Conceded, stated in the simulation report 1; Answered by design, with the dependency conceded. Update 7… 1; Conceded, stated in the litepaper, with the dial explained 1; Closed by spec 1; Conceded by decision, stated in the design doc 1; Answered by design, with the founder's edge conceded 1; Answered by design, with a metrics caveat 1; Closed by removal, 3 October 2026 1; Conceded, stated in the litepaper 1; Conceded, stated in the design doc 1; Measured on the live node line, and the overlay does NOT… 1; Conceded, stated in the simulation 1; Conceded in part, labelled, stated 1; Fixed in the node 1; Answered with evidence for the largest body the rules allow 1; Spec fixed 1; Fixed in the proving code 1; Fixed in the spec 1; Rule fixed 1; Rule written 1; Fixed, logged 1; Answered with evidence for the test half 1; Fixed in the node and shipped, rule not yet activated on… 1; Simulation half run 1; Answered with evidence for all four 1; Written 1; Designed 1; Fixed and confirmed 1; Rolled out 1; Conceded by decision 1; Conceded, scheduled, stated 1; Conceded, contained by rule, stated 1; Fixed on a branch and verified locally 1; Answered with evidence and stated 1; Answered with evidence for PC 2 1; Answered by design and with evidence 1; Fixed, stated; restated 1; Fixed, stated; restated further 1; Fixed in part, finding bounded, stated 1; Fixed as a genesis lever, measurement owed 1. | Id | Claim or criticism | Status | What was done | Evidence | |---|---|---|---|---| @@ -193,6 +193,7 @@ Generated by `tools/ledger/export-public.mjs` from `docs/fud-ledger.md`; a gate | N2 | Any peer could crash any pruned node with a sync request below its retention | Fixed | `SyncManager::antipast_hashes_between` (the IBD headers path, `RequestHeaders`) unwrapped the GHOSTDAG reads of the requested low block and of every chain block of the walk; a pruned node holds no GHOSTDAG data below… | unit test `a_sync_request_below_retention_is_an_error_not_a_panic` (a chain of six headers, the genesis's GHOSTDAG… | | P23 | An unwound transaction leaves the node's view until its sender resends it | Fixed on a branch, pending merge | a fork a commit (the P23 commit, on the merge of `ledger-fixes` and `ledger-fixes-2` onto the 0.3.11 fork tip a commit); `EvmPool::on_chain_removed` (igneum/exec/src/pool.rs) and `ExecService::requeue_unwound`… | [igneum/exec/src/pool.rs](../igneum/exec/src/pool.rs) | | AP-F8-1 | A load whose source was last written by `or`, `mul` or `mulhi` makes a cross-hash hot set | Fixed in part, finding bounded, stated | Class v4 sub-version 3 (igneum-pow a commit, the audit-freeze tag) is frozen with the dataflow rule, the shared-operand rule, the 0.98 ratio and the total draw; the in-house pass's F8 re-gate reads 60 of 64 seeds under… | [docs/analysis/ca3-v4-uniform.md](../docs/analysis/ca3-v4-uniform.md) | +| AP-F8-4 | The program id's derivation text omitted generator 4's sub-version suffix (interoperability, documentation; no object change) | Fixed | The id and its printed derivation come from one byte recipe (`generator::IdRecipe`, `program_id_recipe`, `program_id_class_recipe`, `Program::program_id_derivation`), so the two cannot drift; the emitter prints that… | [igneum-pow/tests/derivation.rs](../igneum-pow/tests/derivation.rs) | | GF1 | A post-quantum signature scheme would need a hard fork, and every vote key is a public BLS12-381 point | Fixed | The byte costs nothing now and a fork later. | none named | | GF2 | A vote key cannot move: a miner who changes keys re-earns 30 days of weight, and so does the post-quantum migration | Fixed | The successor inherits the window, not a fresh one, so a key rotation costs no weight and the migration of GF1 is one item per key. | none named | | GF3 | A 256 MB on-chip cache makes the lottery hash 2 to 3x cheaper for the card that has it, and the cache size is a constant | Fixed as a genesis lever, measurement owed | Consumer LLC is 96 to 128 MB today and datacentre 256 MB (`chip-model-v3`, approximate), so the shortcut is a datacentre card's today and a consumer card's in a generation or two. | none named | diff --git a/docs/spec/01-lottery-hash.md b/docs/spec/01-lottery-hash.md index 1366440d1..18d917311 100644 --- a/docs/spec/01-lottery-hash.md +++ b/docs/spec/01-lottery-hash.md @@ -178,7 +178,7 @@ Implemented (`igneum-pow/src/accept.rs`, `proto-metal/main.swift`; ledger M6 Fix Attempts. Attempt 0 of a program seed `b` (the 32-byte epoch seed, or the UTF-8 of a seed string) is the candidate drawn from `seed_words_from_bytes(b)`. If it fails, attempt `k = 1, 2, ...` is drawn from `seed_words_from_bytes(b || k_le32)`; the first accepted candidate is the program of the epoch. Measured rejection rate under this generator: 5.14 percent over 100,000 seeds (census section 7) and the 20,000-seed confirmation of `docs/bench-log.md` (4 October 2026), so the probability that 32 consecutive candidates fail is below 2^-136, and an implementation MAY treat 32 consecutive failures as a consensus fault (`MAX_ATTEMPTS`). -Program id. `FNV-1a-64("igneum-program/" || generator_le32 || seed words as little-endian bytes || attempt_le32)` with `generator = 2` under class v2 and `generator = 3` under class v3, written into every pack. Two implementations that agree on the id agree on the generator version, the seed words and the attempt. +Program id. `FNV-1a-64("igneum-program/" || generator_le32 || seed words as little-endian bytes || attempt_le32 || suffix)` with `generator = 2` under class v2 and `generator = 3` under class v3 (no suffix), and `generator = 4` under class v4 with the suffix `"sub/" || sub_version_le16` (the class v4 stream's sub-version, 3 since 7 October 2026: `PROGRAM_SUBVERSION_V4` in `generator.rs`, `IGNEUM_PROGRAM_SUBVERSION` in program.h, `"sub_version"` in program.json; without the suffix Devnet 3's epoch-0 id reads 30956569d8f3d8d7 where the chain and the packs say fce15bf61030be57); class v5 is `generator = 5` with no suffix. A program of a non-default class (a read-width, scratch, mixer, derivation, era, shadow or hot rung other than class v4's rung 0) uses the tag `"igneum-program-rw/"` and appends the class's fields after `attempt_le32` (`generator::program_id_class_recipe`). Every pack prints its own derivation as `program_id_derivation` in program.json, built from the same byte recipe the id is hashed from (`generator::IdRecipe`), and `igneum-pow/tests/derivation.rs` re-derives every pinned pack's id from that text. Two implementations that agree on the id agree on the generator version, the seed words, the attempt and, under class v4, the sub-version. Why the closed form: the test is then a pure function of the program (no cache, no day), costs 1.3 to 3.4 ms on one core, and the census checked on 100,000 programs that its verdict agrees with the memory-hard dataset's on all but 39 threshold-edge cases (section 7.3). What the three parts catch: (a) the empty-list fallback of 1.4.3; (b) registers that saturate to all ones (2.4 percent of candidates); (c) zero-absorbing register sets, lane-constant load sites, output bias and value-level address repeats (2.1 percent). Not in the rule, and why: a contraction as the last write (80 percent of programs) and the `or` count are too common and (c) already catches the cases that matter; the load critical path is a hash-rate question, not a weakness. diff --git a/igneum-pow/src/emit.rs b/igneum-pow/src/emit.rs index 8d93a59a3..c8c99693f 100644 --- a/igneum-pow/src/emit.rs +++ b/igneum-pow/src/emit.rs @@ -1812,7 +1812,7 @@ pub fn program_json(p: &Program, day: &str, ds: &DatasetSource) -> String { s.push_str(&format!(" \"generator\": {},\n", p.generator)); s.push_str(&format!(" \"attempt\": {},\n", p.attempt)); s.push_str(&format!(" \"program_id\": {},\n", jhex64(p.program_id()))); - s.push_str(" \"program_id_derivation\": \"FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32\",\n"); + s.push_str(&format!(" \"program_id_derivation\": {},\n", jstr(&p.program_id_derivation()))); s.push_str(&format!( " \"dataset_mode\": {},\n", jstr(if memhard.is_some() { "memory-hard" } else { "closed-form" }) diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs index 9c2ebe05a..a6698e89f 100644 --- a/igneum-pow/src/generator.rs +++ b/igneum-pow/src/generator.rs @@ -1077,6 +1077,17 @@ impl Program { } } + /// The derivation of [`Program::program_id`] as text, from the same byte recipe (program.json's + /// "program_id_derivation"; spec 01 section 1.4.6). + pub fn program_id_derivation(&self) -> String { + 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 { + program_id_recipe(self.generator, &self.seed, self.attempt).text() + } else { + program_id_class_recipe(self.generator, &self.seed, self.attempt, &self.class).text() + } + } + /// The program class of this program, from its generator version (3 = v3, 4 = v4, everything else v2). pub fn program_class(&self) -> ProgramClass { match self.generator { @@ -1087,24 +1098,61 @@ impl Program { } } -pub fn program_id(generator: u32, seed: &[u32; 8], attempt: u32) -> u64 { - let mut b = Vec::with_capacity(PROGRAM_ID_TAG.len() + 4 + 32 + 4 + 6); - b.extend_from_slice(PROGRAM_ID_TAG); - b.extend_from_slice(&generator.to_le_bytes()); - for w in seed { - b.extend_from_slice(&w.to_le_bytes()); +/// The byte recipe of a program id and the text that states it: the bytes FNV-1a 64 hashes and, part by part, the label +/// of each part (`'literal'` or `field_le32`), so the derivation printed in program.json is built from the same list the id +/// is hashed from and the two cannot drift (the documentation finding of 7 October 2026: program.json and spec 1.4.6 said +/// the plain form while generator 4 appended `'sub/' || sub_version_le16`, so a client written from the text derived another +/// id). `tests/derivation.rs` re-derives every pinned pack's id from its own derivation string. +#[derive(Clone, Debug, Default)] +pub struct IdRecipe { + pub bytes: Vec, + parts: Vec, +} + +impl IdRecipe { + fn lit(&mut self, s: &[u8]) { + self.bytes.extend_from_slice(s); + self.parts.push(format!("'{}'", String::from_utf8_lossy(s))); } - b.extend_from_slice(&attempt.to_le_bytes()); + fn field(&mut self, bytes: &[u8], label: &str) { + self.bytes.extend_from_slice(bytes); + self.parts.push(label.to_string()); + } + /// The derivation as program.json prints it: `FNV-1a 64 over || || ...`. + pub fn text(&self) -> String { + format!("FNV-1a 64 over {}", self.parts.join(" || ")) + } + /// The id: FNV-1a 64 over the bytes. + pub fn id(&self) -> u64 { + fnv1a64(&self.bytes) + } +} + +/// The recipe of [`program_id`]. +pub fn program_id_recipe(generator: u32, seed: &[u32; 8], attempt: u32) -> IdRecipe { + let mut r = IdRecipe::default(); + r.lit(PROGRAM_ID_TAG); + r.field(&generator.to_le_bytes(), "generator_le32"); + let mut sw = Vec::with_capacity(32); + for w in seed { + sw.extend_from_slice(&w.to_le_bytes()); + } + r.field(&sw, "seed_words as little-endian bytes"); + r.field(&attempt.to_le_bytes(), "attempt_le32"); if generator == GENERATOR_VERSION_V4 { // The class v4 sub-version (AP-F8-1 amendment, 7 October 2026): `"sub/" || sub_version as little-endian u16` // appended for generator 4 only, so a binary from before the load-source rule (sub-version 0, no suffix) and // one after it never share a program id for one seed; the node's id check then catches a split. v2 and v3 // ids are byte-identical. The node reads the sub-version from [`PROGRAM_SUBVERSION_V4`]; packs carry it as // IGNEUM_PROGRAM_SUBVERSION and program.json "sub_version". - b.extend_from_slice(b"sub/"); - b.extend_from_slice(&PROGRAM_SUBVERSION_V4.to_le_bytes()); + r.lit(b"sub/"); + r.field(&PROGRAM_SUBVERSION_V4.to_le_bytes(), "sub_version_le16"); } - fnv1a64(&b) + r +} + +pub fn program_id(generator: u32, seed: &[u32; 8], attempt: u32) -> u64 { + program_id_recipe(generator, seed, attempt).id() } /// The sub-version of class v4's program stream, in every generator-4 program id and pack (AP-F8-3: 3 = the @@ -1118,53 +1166,60 @@ pub const PROGRAM_ID_TAG_RW: &[u8] = b"igneum-program-rw/"; /// The program id of a non-default class: the tag, then the same fields as [`program_id`], then the three mix /// percentages and the slot count as bytes. -pub fn program_id_class(generator: u32, seed: &[u32; 8], attempt: u32, class: &LoadClass) -> u64 { - let mut b = Vec::with_capacity(PROGRAM_ID_TAG_RW.len() + 4 + 32 + 4 + 4); - b.extend_from_slice(PROGRAM_ID_TAG_RW); - b.extend_from_slice(&generator.to_le_bytes()); +/// The recipe of [`program_id_class`]. +pub fn program_id_class_recipe(generator: u32, seed: &[u32; 8], attempt: u32, class: &LoadClass) -> IdRecipe { + let mut r = IdRecipe::default(); + r.lit(PROGRAM_ID_TAG_RW); + r.field(&generator.to_le_bytes(), "generator_le32"); + let mut sw = Vec::with_capacity(32); for w in seed { - b.extend_from_slice(&w.to_le_bytes()); + sw.extend_from_slice(&w.to_le_bytes()); } - b.extend_from_slice(&attempt.to_le_bytes()); - b.extend_from_slice(&class.mix); - b.push(class.load_slots); + r.field(&sw, "seed_words as little-endian bytes"); + r.field(&attempt.to_le_bytes(), "attempt_le32"); + r.field(&class.mix, "mix[3]"); + r.field(&[class.load_slots], "load_slots_u8"); if let Some(k) = class.scratch { - b.extend_from_slice(b"scratch/"); - b.push(k); - b.push(class.scratch_kb); + r.lit(b"scratch/"); + r.field(&[k], "scratch_k_u8"); + r.field(&[class.scratch_kb], "scratch_kb_u8"); } if class.mixer_mult != 1 || class.growth { // Counter ASIC 2.0: the mixer multiplier and the growth rule are part of the construction, so a program of // the same seed under a different mixer carries a different id (under the v3 seam the id is // program_id(3, seed, attempt) and this branch is not taken) - b.extend_from_slice(b"mixer/"); - b.push(class.mixer_mult); - b.push(class.growth as u8); + r.lit(b"mixer/"); + r.field(&[class.mixer_mult], "mixer_mult_u8"); + r.field(&[class.growth as u8], "growth_u8"); } if class.derive_len != 0 { // Counter ASIC 3.0 item 2: the derivation program's length is part of the construction - b.extend_from_slice(b"derive/"); - b.extend_from_slice(&class.derive_len.to_le_bytes()); + r.lit(b"derive/"); + r.field(&class.derive_len.to_le_bytes(), "derive_len_le16"); } if let Some(e) = class.era { - b.extend_from_slice(b"era/"); - b.extend_from_slice(&e.id_bytes()); + r.lit(b"era/"); + r.field(&e.id_bytes(), "era_id_bytes (allowed[3] || width_words_u8 || stride_mul_le32 || stride_rot_le32 || interleave[4])"); } if let Some(sh) = class.shadow { // Counter ASIC 3.0 item 8: the shadow block's size and repeat count are part of the construction - b.extend_from_slice(b"shadow/"); - b.extend_from_slice(&sh.instrs.to_le_bytes()); - b.extend_from_slice(&sh.reps.to_le_bytes()); + r.lit(b"shadow/"); + r.field(&sh.instrs.to_le_bytes(), "shadow_instrs_le16"); + r.field(&sh.reps.to_le_bytes(), "shadow_reps_le16"); } if let Some(h) = class.hot { - b.extend_from_slice(b"hot/"); - b.push(h.mb); - b.push(h.k); + r.lit(b"hot/"); + r.field(&[h.mb], "hot_mb_u8"); + r.field(&[h.k], "hot_k_u8"); if h.added { - b.extend_from_slice(b"added"); + r.lit(b"added"); } } - fnv1a64(&b) + r +} + +pub fn program_id_class(generator: u32, seed: &[u32; 8], attempt: u32, class: &LoadClass) -> u64 { + program_id_class_recipe(generator, seed, attempt, class).id() } /// Weights of the ten non-load families under version 2, in draw order. Sum 75. The load family has no diff --git a/igneum-pow/tests/derivation.rs b/igneum-pow/tests/derivation.rs new file mode 100644 index 000000000..71db0805d --- /dev/null +++ b/igneum-pow/tests/derivation.rs @@ -0,0 +1,142 @@ +//! Every pinned pack's program id re-derived from its own `program_id_derivation` text (the documentation finding of +//! 7 October 2026, the in-house pass adv-accept-3: program.json and spec 1.4.6 stated the plain form while generator 4 +//! appends `'sub/' || sub_version_le16`, so a client written from the text derived 30956569d8f3d8d7 for Devnet 3's epoch +//! 0 where the chain says fce15bf61030be57). The text is parsed part by part and the bytes come from the pack's own fields +//! (generator, attempt, seed_words, sub_version, load_class), never from the code's id function, so the test fails the +//! moment the printed derivation and the hashed bytes disagree. + +use igneum_pow::generator::LoadClass; +use serde_json::Value; +use std::path::{Path, PathBuf}; + +fn fnv1a64(b: &[u8]) -> u64 { + let mut h: u64 = 0xcbf2_9ce4_8422_2325; + for &x in b { + h ^= x as u64; + h = h.wrapping_mul(0x0000_0100_0000_01b3); + } + h +} + +fn hex64(v: &Value) -> u64 { + u64::from_str_radix(v.as_str().unwrap().trim_start_matches("0x"), 16).unwrap() +} + +/// The bytes a derivation part names, from the pack's own fields. +fn part_bytes(part: &str, j: &Value, class: Option<&LoadClass>) -> Vec { + if let Some(lit) = part.strip_prefix('\'').and_then(|p| p.strip_suffix('\'')) { + return lit.as_bytes().to_vec(); + } + let c = || class.unwrap_or_else(|| panic!("part {part} needs the pack's load_class")); + match part { + "generator_le32" => (j["generator"].as_u64().unwrap() as u32).to_le_bytes().to_vec(), + "attempt_le32" => (j["attempt"].as_u64().unwrap() as u32).to_le_bytes().to_vec(), + "seed_words as little-endian bytes" => j["seed_words"].as_array().unwrap().iter().flat_map(|w| (hex64(w) as u32).to_le_bytes()).collect(), + "sub_version_le16" => (j["sub_version"].as_u64().expect("a generator-4 pack carries sub_version") as u16).to_le_bytes().to_vec(), + "mix[3]" => c().mix.to_vec(), + "load_slots_u8" => vec![c().load_slots], + "scratch_k_u8" => vec![c().scratch.unwrap()], + "scratch_kb_u8" => vec![c().scratch_kb], + "mixer_mult_u8" => vec![c().mixer_mult], + "growth_u8" => vec![c().growth as u8], + "derive_len_le16" => c().derive_len.to_le_bytes().to_vec(), + "shadow_instrs_le16" => c().shadow.unwrap().instrs.to_le_bytes().to_vec(), + "shadow_reps_le16" => c().shadow.unwrap().reps.to_le_bytes().to_vec(), + "hot_mb_u8" => vec![c().hot.unwrap().mb], + "hot_k_u8" => vec![c().hot.unwrap().k], + other => panic!("derivation part {other:?} is not one this test knows; teach it the field before pinning such a pack"), + } +} + +fn rederive(dir: &Path) -> (u64, u64, String) { + let j: Value = serde_json::from_str(&std::fs::read_to_string(dir.join("program.json")).unwrap()).unwrap(); + let text = j["program_id_derivation"].as_str().unwrap().to_string(); + let parts = text.strip_prefix("FNV-1a 64 over ").unwrap_or_else(|| panic!("{}: the derivation does not start with 'FNV-1a 64 over '", dir.display())); + let class = j["load_class"].as_str().and_then(LoadClass::parse); + let mut b = Vec::new(); + for part in parts.split(" || ") { + b.extend(part_bytes(part.trim(), &j, class.as_ref())); + } + (fnv1a64(&b), hex64(&j["program_id"]), text) +} + +fn pinned_packs() -> Vec { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda"); + let mut out = Vec::new(); + for sub in ["packs-ca3-v4", "packs-ca3-shadow"] { + let mut v: Vec<_> = std::fs::read_dir(root.join(sub)).unwrap().map(|e| e.unwrap().path()).filter(|p| p.join("program.json").is_file()).collect(); + v.sort(); + out.extend(v); + } + out.push(root.join("packs/igneum-devnet-v4-epoch0")); + assert!(out.len() >= 18, "the pinned packs are missing; this test never skips ({} found)", out.len()); + out +} + +#[test] +fn every_pinned_pack_id_rederives_from_its_own_derivation_text() { + let mut seen_sub = false; + for dir in pinned_packs() { + let (derived, pinned, text) = rederive(&dir); + assert_eq!(derived, pinned, "{}: the id re-derived from \"{text}\" is {derived:016x}, the pack says {pinned:016x}", dir.display()); + if text.contains("'sub/'") { + seen_sub = true; + } + } + assert!(seen_sub, "at least one generator-4 pack states the 'sub/' || sub_version_le16 suffix"); +} + +#[test] +fn the_class_v4_derivation_states_the_suffix_and_the_plain_text_derives_another_id() { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda/packs-ca3-v4/v4-devnet-epoch0"); + let j: Value = serde_json::from_str(&std::fs::read_to_string(root.join("program.json")).unwrap()).unwrap(); + let text = j["program_id_derivation"].as_str().unwrap(); + assert!(text.ends_with("attempt_le32 || 'sub/' || sub_version_le16"), "{text}"); + // the known-failed case: the text as printed before 7 October 2026 (no suffix) derives a different id + let plain = "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32"; + let mut b = Vec::new(); + for part in plain.strip_prefix("FNV-1a 64 over ").unwrap().split(" || ") { + b.extend(part_bytes(part, &j, None)); + } + assert_ne!(fnv1a64(&b), hex64(&j["program_id"]), "the plain text must not derive the pinned id (it did before the fix)"); + assert_eq!(hex64(&j["program_id"]), 0xa785_0016_87d8_688a); +} + +/// The record maker (ignored in the suite): prints `DERIVATION ` for every pack directory under proto-cuda, +/// the program rebuilt as the pack tests rebuild it (generator 3 with era bytes under class v2's era layout for packs-ca2-era, +/// generator 3 through the v3 seam otherwise, generator 2 with its load class, plain generator 2), so program.json's line can +/// be set from the code's own recipe after a derivation change. `cargo test --release --test derivation -- --ignored --nocapture`. +#[test] +#[ignore] +fn print_every_pack_derivation() { + use igneum_pow::generator::{generate_era, generate_from_seed_bytes, generate_from_seed_bytes_class, generate_from_seed_bytes_program_class, ProgramClass, GENERATOR_VERSION_V3}; + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda"); + let mut dirs: Vec = std::fs::read_dir(&root).unwrap().map(|e| e.unwrap().path()).filter(|p| p.file_name().unwrap().to_str().unwrap().starts_with("packs")).flat_map(|g| std::fs::read_dir(g).unwrap().map(|e| e.unwrap().path())).filter(|p| p.join("program.json").is_file()).collect(); + dirs.sort(); + for dir in dirs { + let j: Value = serde_json::from_str(&std::fs::read_to_string(dir.join("program.json")).unwrap()).unwrap(); + let seed = j["seed"].as_str().unwrap(); + let seed_bytes = igneum_pow::bind::unhex(j["seed_bytes"].as_str().unwrap()).unwrap(); + let gen = j["generator"].as_u64().unwrap() as u32; + let era = j.get("era_seed_bytes").and_then(|v| v.as_str()).map(|h| igneum_pow::bind::unhex(h).unwrap()); + let lc = j.get("load_class").and_then(|c| c.as_str()); + let program = if gen == GENERATOR_VERSION_V3 && dir.parent().unwrap().file_name().unwrap() == "packs-ca2-era" { + let allowed: Vec = j["era"]["allowed_widths"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap() as u8).collect(); + generate_era(seed, &seed_bytes, LoadClass::V2, era.as_deref().unwrap(), &allowed) + } else if gen == GENERATOR_VERSION_V3 { + generate_from_seed_bytes_program_class(seed, &seed_bytes, ProgramClass::V3, era.as_deref()) + } else if gen == 4 { + generate_from_seed_bytes_program_class(seed, &seed_bytes, ProgramClass::V4, era.as_deref()) + } else if let Some(c) = lc { + let Some(class) = LoadClass::parse(c) else { + println!("SKIP {} load_class {c:?} is a name the parser no longer accepts (an old experiment's pack); its derivation line stands as written", dir.strip_prefix(&root).unwrap().display()); + continue; + }; + generate_from_seed_bytes_class(seed, &seed_bytes, class) + } else { + generate_from_seed_bytes(seed, &seed_bytes) + }; + println!("DERIVATION {} {:016x} {}", dir.strip_prefix(&root).unwrap().display(), program.program_id(), program.program_id_derivation()); + assert_eq!(program.program_id(), hex64(&j["program_id"]), "{}: the rebuilt program's id is the pack's", dir.display()); + } +} diff --git a/packaging/ota/publish-jobs.sh b/packaging/ota/publish-jobs.sh index a3d1f9c65..e1fba7a81 100755 --- a/packaging/ota/publish-jobs.sh +++ b/packaging/ota/publish-jobs.sh @@ -9,7 +9,7 @@ # public key compiled into src/manifest.rs, runs each job that targets it ONCE per id, and reports to the log intake # as run_id job-- (read back with tools/jobs.mjs). # -# packaging/ota/publish-jobs.sh add --kind run --target 1ccfe586 --script path.ps1 [--elevated] [--stop-miners] \ +# packaging/ota/publish-jobs.sh add --kind run --target 1ccfe586 --script path.ps1 [--stop-miners] \ (--elevated is REFUSED: the 7 Oct 2026 no-prompt rule) # [--cards-off key,key] [--timeout-minutes 60] [--shell powershell|bash] --title "..." [--expires-hours 48] [--deploy] # (--cards-off: the RUNNER switches these cards off through the app's own card path before the script and puts them # back exactly on any exit, done, failed, timeout, aborted or the app quitting; keys with or without the device @@ -72,7 +72,15 @@ while [ $# -gt 0 ]; do --expires-hours) EXPIRES_H="$2"; shift 2 ;; --script) SCRIPT="$2"; shift 2 ;; --shell) SHELL_KIND="$2"; shift 2 ;; - --elevated) ELEVATED=1; shift ;; + --elevated) + # STANDING RULE (the founder through main, 7 October 2026, 18:5x UTC): no PC job may raise a UAC prompt or need a click, ever. A run + # job published --elevated launches the script through an administrator prompt on the PC (the 18:27Z job + # run-ca3-pc1-v4-eff-5090-20261007 waited 2 minutes for a click and died with exit 251, the card switched off for nothing). + # Anything needing rights goes through the installed app's Igneum Power Helper task (app/igneum-app/src/powertask.rs, + # present when Power control is on); a job reads the task's presence first and, without it, reports + # "no elevation path: Power control off on " with the measured-only rows it can still take. The flag is refused here. + echo "publish-jobs: --elevated is refused (standing rule of 7 October 2026: no PC job raises a UAC prompt; use the Igneum Power Helper task from the script, see app/igneum-app/src/powertask.rs and tools/ca3-v4-amend/pc1-v4-efficiency.ps1)" >&2 + exit 3 ;; --stop-miners) STOP_MINERS=1; shift ;; --cards-off) CARDS_OFF="$2"; shift 2 ;; --timeout-minutes) TIMEOUT_MIN="$2"; shift 2 ;; diff --git a/proto-cuda/packs-ca2-hot/hot32k4/program.json b/proto-cuda/packs-ca2-hot/hot32k4/program.json index 6521857a1..7c730e66f 100644 --- a/proto-cuda/packs-ca2-hot/hot32k4/program.json +++ b/proto-cuda/packs-ca2-hot/hot32k4/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x3370a4466e5f322c", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'hot/' || hot_mb_u8 || hot_k_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca2-hot/hot32k4a/program.json b/proto-cuda/packs-ca2-hot/hot32k4a/program.json index 0d46fc054..557b4becc 100644 --- a/proto-cuda/packs-ca2-hot/hot32k4a/program.json +++ b/proto-cuda/packs-ca2-hot/hot32k4a/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0xd40c36ab07d82002", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'hot/' || hot_mb_u8 || hot_k_u8 || 'added'", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca2-hot/hot64k2/program.json b/proto-cuda/packs-ca2-hot/hot64k2/program.json index 5d402be4d..16600ba0f 100644 --- a/proto-cuda/packs-ca2-hot/hot64k2/program.json +++ b/proto-cuda/packs-ca2-hot/hot64k2/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x322a62466d49ff66", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'hot/' || hot_mb_u8 || hot_k_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca2-hot/hot64k4/program.json b/proto-cuda/packs-ca2-hot/hot64k4/program.json index 436d58d65..4b842876b 100644 --- a/proto-cuda/packs-ca2-hot/hot64k4/program.json +++ b/proto-cuda/packs-ca2-hot/hot64k4/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x322a64466d4a02cc", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'hot/' || hot_mb_u8 || hot_k_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca2-hot/hot64k4a/program.json b/proto-cuda/packs-ca2-hot/hot64k4a/program.json index 1fc72c2d6..59bc7cbfe 100644 --- a/proto-cuda/packs-ca2-hot/hot64k4a/program.json +++ b/proto-cuda/packs-ca2-hot/hot64k4a/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x7f581216e95eb062", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'hot/' || hot_mb_u8 || hot_k_u8 || 'added'", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca2-hot/hot64k8/program.json b/proto-cuda/packs-ca2-hot/hot64k8/program.json index 63e52f1c4..fb7851437 100644 --- a/proto-cuda/packs-ca2-hot/hot64k8/program.json +++ b/proto-cuda/packs-ca2-hot/hot64k8/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x322a68466d4a0998", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'hot/' || hot_mb_u8 || hot_k_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca2-hot/hot96k4/program.json b/proto-cuda/packs-ca2-hot/hot96k4/program.json index c0d414cfe..893f13d9a 100644 --- a/proto-cuda/packs-ca2-hot/hot96k4/program.json +++ b/proto-cuda/packs-ca2-hot/hot96k4/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x329724466da667ec", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'hot/' || hot_mb_u8 || hot_k_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca2-hot/hot96k4a/program.json b/proto-cuda/packs-ca2-hot/hot96k4a/program.json index a11407837..ddb747598 100644 --- a/proto-cuda/packs-ca2-hot/hot96k4a/program.json +++ b/proto-cuda/packs-ca2-hot/hot96k4a/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x2ff8e52c952a62c2", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'hot/' || hot_mb_u8 || hot_k_u8 || 'added'", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca2-mixer/mx4-devnet-epoch0/program.json b/proto-cuda/packs-ca2-mixer/mx4-devnet-epoch0/program.json index dcce05422..64df83d7e 100644 --- a/proto-cuda/packs-ca2-mixer/mx4-devnet-epoch0/program.json +++ b/proto-cuda/packs-ca2-mixer/mx4-devnet-epoch0/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0xa6892c64d9ddd7f7", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8", "dataset_mode": "memory-hard", "seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000", "seed_bytes": "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07", diff --git a/proto-cuda/packs-ca2-mixer/mx4-genesis/program.json b/proto-cuda/packs-ca2-mixer/mx4-genesis/program.json index 8b247276e..be419a644 100644 --- a/proto-cuda/packs-ca2-mixer/mx4-genesis/program.json +++ b/proto-cuda/packs-ca2-mixer/mx4-genesis/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x951b89584750bd75", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-derive/dr736-devnet-epoch0/program.json b/proto-cuda/packs-ca3-derive/dr736-devnet-epoch0/program.json index c4633cff9..c61c2918d 100644 --- a/proto-cuda/packs-ca3-derive/dr736-devnet-epoch0/program.json +++ b/proto-cuda/packs-ca3-derive/dr736-devnet-epoch0/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x7f4a5ca0a3637820", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'derive/' || derive_len_le16", "dataset_mode": "memory-hard", "seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000", "seed_bytes": "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07", diff --git a/proto-cuda/packs-ca3-derive/dr736-genesis/program.json b/proto-cuda/packs-ca3-derive/dr736-genesis/program.json index 7a0ab9fee..4da56ff44 100644 --- a/proto-cuda/packs-ca3-derive/dr736-genesis/program.json +++ b/proto-cuda/packs-ca3-derive/dr736-genesis/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x72c1d8048aef9542", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'derive/' || derive_len_le16", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-shadow/sh1024x3/program.json b/proto-cuda/packs-ca3-shadow/sh1024x3/program.json index 48e5456a5..20595066c 100644 --- a/proto-cuda/packs-ca3-shadow/sh1024x3/program.json +++ b/proto-cuda/packs-ca3-shadow/sh1024x3/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0xb42f8c8549967581", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'shadow/' || shadow_instrs_le16 || shadow_reps_le16", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-shadow/sh256x13/program.json b/proto-cuda/packs-ca3-shadow/sh256x13/program.json index b52604393..541c6e4e1 100644 --- a/proto-cuda/packs-ca3-shadow/sh256x13/program.json +++ b/proto-cuda/packs-ca3-shadow/sh256x13/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x9a5be3853b011f6c", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'shadow/' || shadow_instrs_le16 || shadow_reps_le16", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-shadow/sh256x2/program.json b/proto-cuda/packs-ca3-shadow/sh256x2/program.json index 4fb063297..8c88f26a7 100644 --- a/proto-cuda/packs-ca3-shadow/sh256x2/program.json +++ b/proto-cuda/packs-ca3-shadow/sh256x2/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x9a3d4d853ae722fb", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'shadow/' || shadow_instrs_le16 || shadow_reps_le16", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-shadow/sh256x27/program.json b/proto-cuda/packs-ca3-shadow/sh256x27/program.json index a4c1fe380..2ee1be184 100644 --- a/proto-cuda/packs-ca3-shadow/sh256x27/program.json +++ b/proto-cuda/packs-ca3-shadow/sh256x27/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x9a8b77853b298baa", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'shadow/' || shadow_instrs_le16 || shadow_reps_le16", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-shadow/sh256x40/program.json b/proto-cuda/packs-ca3-shadow/sh256x40/program.json index ebe2a9edf..288c52ea9 100644 --- a/proto-cuda/packs-ca3-shadow/sh256x40/program.json +++ b/proto-cuda/packs-ca3-shadow/sh256x40/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x9abe71853b54db11", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'shadow/' || shadow_instrs_le16 || shadow_reps_le16", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-shadow/sh256x53/program.json b/proto-cuda/packs-ca3-shadow/sh256x53/program.json index 5e0441f8f..6b088946d 100644 --- a/proto-cuda/packs-ca3-shadow/sh256x53/program.json +++ b/proto-cuda/packs-ca3-shadow/sh256x53/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x9ae3d3853b749dd4", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'shadow/' || shadow_instrs_le16 || shadow_reps_le16", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-shadow/sh256x7/program.json b/proto-cuda/packs-ca3-shadow/sh256x7/program.json index 558ee55a5..6696b8f4a 100644 --- a/proto-cuda/packs-ca3-shadow/sh256x7/program.json +++ b/proto-cuda/packs-ca3-shadow/sh256x7/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x9a477f853aefcc76", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'shadow/' || shadow_instrs_le16 || shadow_reps_le16", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-shadow/sh256x88/program.json b/proto-cuda/packs-ca3-shadow/sh256x88/program.json index 477aa8dc9..0dc1c8171 100644 --- a/proto-cuda/packs-ca3-shadow/sh256x88/program.json +++ b/proto-cuda/packs-ca3-shadow/sh256x88/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x9b6191853bdf72c1", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'shadow/' || shadow_instrs_le16 || shadow_reps_le16", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-shadow/sh64x52/program.json b/proto-cuda/packs-ca3-shadow/sh64x52/program.json index 8cd722d50..d3e948bdd 100644 --- a/proto-cuda/packs-ca3-shadow/sh64x52/program.json +++ b/proto-cuda/packs-ca3-shadow/sh64x52/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x90cd909ac3e78d4a", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'mixer/' || mixer_mult_u8 || growth_u8 || 'shadow/' || shadow_instrs_le16 || shadow_reps_le16", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-ca3-v4/v4-devnet-epoch0/program.json b/proto-cuda/packs-ca3-v4/v4-devnet-epoch0/program.json index 16211bc41..86a4c306d 100644 --- a/proto-cuda/packs-ca3-v4/v4-devnet-epoch0/program.json +++ b/proto-cuda/packs-ca3-v4/v4-devnet-epoch0/program.json @@ -3,7 +3,7 @@ "generator": 4, "attempt": 1, "program_id": "0xa785001687d8688a", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || 'sub/' || sub_version_le16", "dataset_mode": "memory-hard", "seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000", "seed_bytes": "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07", diff --git a/proto-cuda/packs-ca3-v4/v4-era-0/program.json b/proto-cuda/packs-ca3-v4/v4-era-0/program.json index e48c4856f..e2355ca4b 100644 --- a/proto-cuda/packs-ca3-v4/v4-era-0/program.json +++ b/proto-cuda/packs-ca3-v4/v4-era-0/program.json @@ -3,7 +3,7 @@ "generator": 4, "attempt": 1, "program_id": "0xa785001687d8688a", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || 'sub/' || sub_version_le16", "dataset_mode": "memory-hard", "seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000", "seed_bytes": "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07", diff --git a/proto-cuda/packs-ca3-v4/v4-era-1/program.json b/proto-cuda/packs-ca3-v4/v4-era-1/program.json index 4fbdfb5cf..62e265f0e 100644 --- a/proto-cuda/packs-ca3-v4/v4-era-1/program.json +++ b/proto-cuda/packs-ca3-v4/v4-era-1/program.json @@ -3,7 +3,7 @@ "generator": 4, "attempt": 1, "program_id": "0xa785001687d8688a", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || 'sub/' || sub_version_le16", "dataset_mode": "memory-hard", "seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000", "seed_bytes": "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07", diff --git a/proto-cuda/packs-ca3-v4/v4-era-2/program.json b/proto-cuda/packs-ca3-v4/v4-era-2/program.json index a0ee358c4..2fa2d1001 100644 --- a/proto-cuda/packs-ca3-v4/v4-era-2/program.json +++ b/proto-cuda/packs-ca3-v4/v4-era-2/program.json @@ -3,7 +3,7 @@ "generator": 4, "attempt": 1, "program_id": "0xa785001687d8688a", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || 'sub/' || sub_version_le16", "dataset_mode": "memory-hard", "seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000", "seed_bytes": "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07", diff --git a/proto-cuda/packs-ca3-v4/v4-era-3/program.json b/proto-cuda/packs-ca3-v4/v4-era-3/program.json index 1742f600c..6085e54a5 100644 --- a/proto-cuda/packs-ca3-v4/v4-era-3/program.json +++ b/proto-cuda/packs-ca3-v4/v4-era-3/program.json @@ -3,7 +3,7 @@ "generator": 4, "attempt": 1, "program_id": "0xa785001687d8688a", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || 'sub/' || sub_version_le16", "dataset_mode": "memory-hard", "seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000", "seed_bytes": "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07", diff --git a/proto-cuda/packs-ca3-v4/v4-era-4/program.json b/proto-cuda/packs-ca3-v4/v4-era-4/program.json index 8dff7f2a3..a42b9ee2d 100644 --- a/proto-cuda/packs-ca3-v4/v4-era-4/program.json +++ b/proto-cuda/packs-ca3-v4/v4-era-4/program.json @@ -3,7 +3,7 @@ "generator": 4, "attempt": 1, "program_id": "0xa785001687d8688a", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || 'sub/' || sub_version_le16", "dataset_mode": "memory-hard", "seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000", "seed_bytes": "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07", diff --git a/proto-cuda/packs-ca3-v4/v4-era-5/program.json b/proto-cuda/packs-ca3-v4/v4-era-5/program.json index fd1a13e07..6b01e2c33 100644 --- a/proto-cuda/packs-ca3-v4/v4-era-5/program.json +++ b/proto-cuda/packs-ca3-v4/v4-era-5/program.json @@ -3,7 +3,7 @@ "generator": 4, "attempt": 1, "program_id": "0xa785001687d8688a", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || 'sub/' || sub_version_le16", "dataset_mode": "memory-hard", "seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000", "seed_bytes": "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07", diff --git a/proto-cuda/packs-readwidth/scr0k32/program.json b/proto-cuda/packs-readwidth/scr0k32/program.json index 2d28cab4a..0467ec7be 100644 --- a/proto-cuda/packs-readwidth/scr0k32/program.json +++ b/proto-cuda/packs-readwidth/scr0k32/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0xe0b70cd155c28f4b", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'scratch/' || scratch_k_u8 || scratch_kb_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-readwidth/scr2k128/program.json b/proto-cuda/packs-readwidth/scr2k128/program.json index 4b6d69fa5..85fd873d1 100644 --- a/proto-cuda/packs-readwidth/scr2k128/program.json +++ b/proto-cuda/packs-readwidth/scr2k128/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0xe0afe0d155bc25d9", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'scratch/' || scratch_k_u8 || scratch_kb_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-readwidth/scr2k32/program.json b/proto-cuda/packs-readwidth/scr2k32/program.json index e9e8d9cbf..efec3ac1f 100644 --- a/proto-cuda/packs-readwidth/scr2k32/program.json +++ b/proto-cuda/packs-readwidth/scr2k32/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0xe0b080d155bd35b9", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'scratch/' || scratch_k_u8 || scratch_kb_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-readwidth/scr4k128/program.json b/proto-cuda/packs-readwidth/scr4k128/program.json index 4d0d0ad78..3ad7b6307 100644 --- a/proto-cuda/packs-readwidth/scr4k128/program.json +++ b/proto-cuda/packs-readwidth/scr4k128/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0xe0c444d155cd78cf", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'scratch/' || scratch_k_u8 || scratch_kb_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-readwidth/scr4k32/program.json b/proto-cuda/packs-readwidth/scr4k32/program.json index 9bd6cf0f5..2d1537ab9 100644 --- a/proto-cuda/packs-readwidth/scr4k32/program.json +++ b/proto-cuda/packs-readwidth/scr4k32/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0xe0c4a4d155ce1bef", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'scratch/' || scratch_k_u8 || scratch_kb_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-readwidth/scr8k128/program.json b/proto-cuda/packs-readwidth/scr8k128/program.json index 8cbee7a2b..23fd6d0ba 100644 --- a/proto-cuda/packs-readwidth/scr8k128/program.json +++ b/proto-cuda/packs-readwidth/scr8k128/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0xe0d1dcd155d90573", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'scratch/' || scratch_k_u8 || scratch_kb_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-readwidth/scr8k32/program.json b/proto-cuda/packs-readwidth/scr8k32/program.json index 3b0163f57..75d7d9a03 100644 --- a/proto-cuda/packs-readwidth/scr8k32/program.json +++ b/proto-cuda/packs-readwidth/scr8k32/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0xe0d27cd155da1553", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8 || 'scratch/' || scratch_k_u8 || scratch_kb_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-readwidth/w16/program.json b/proto-cuda/packs-readwidth/w16/program.json index 8f563a1b7..ad37ea466 100644 --- a/proto-cuda/packs-readwidth/w16/program.json +++ b/proto-cuda/packs-readwidth/w16/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x6a183165e23fd2bc", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-readwidth/w64/program.json b/proto-cuda/packs-readwidth/w64/program.json index 48e3cfe4b..2bb323be4 100644 --- a/proto-cuda/packs-readwidth/w64/program.json +++ b/proto-cuda/packs-readwidth/w64/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x5c427d666b4ed4f4", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/proto-cuda/packs-readwidth/w64x4/program.json b/proto-cuda/packs-readwidth/w64x4/program.json index de9bee3fa..e857e56d8 100644 --- a/proto-cuda/packs-readwidth/w64x4/program.json +++ b/proto-cuda/packs-readwidth/w64x4/program.json @@ -3,7 +3,7 @@ "generator": 2, "attempt": 0, "program_id": "0x5c4269666b4eb2f8", - "program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32", + "program_id_derivation": "FNV-1a 64 over 'igneum-program-rw/' || generator_le32 || seed_words as little-endian bytes || attempt_le32 || mix[3] || load_slots_u8", "dataset_mode": "memory-hard", "seed": "igneum-genesis", "seed_bytes": "69676e65756d2d67656e65736973", diff --git a/tools/ca3-v4-amend/pc1-amd-family-20261007.ps1 b/tools/ca3-v4-amend/pc1-amd-family-20261007.ps1 new file mode 100644 index 000000000..45dfed52a --- /dev/null +++ b/tools/ca3-v4-amend/pc1-amd-family-20261007.ps1 @@ -0,0 +1,149 @@ +# 7 October 2026 copy for the PC 1 queue: tools/ca3-pc1-amd/pc1-amd-family.ps1 (run e) unchanged except the kit id, since a machine +# runs a fetch id once and the 0.3.21 host build wipes the jobs folder: fetch-ca3-pc1-amd-family-20261007 (the probe rebuilt on +# build-1 from this tree's proto-opencl/family-probe.c). +# Counter ASIC 3.0, PC 1 AMD job 2 (6 October 2026, run e: the 9070 XT ALONE): item 6's step cost of every reserve candidate family of spec +# 1.13.2 on PC 1's RX 9070 XT (machine ae432dc7, gfx1201), docs/plans/counter-asic-3-reserve.md, status file section +# 3 "Item 6" (the OWED AMD column). Run a (run-ca3-pc1-amd-family-20261006, 150 s, exit 0) measured ordinal 0, PC 1's +# integrated gfx1036 (one CU: alu 40.59 G steps/s against the 9070 XT's expected 1,000 to 2,000): the device list parse +# ran a second -match after the capturing one, which overwrote $Matches, so every device read idx 0 and an empty name, +# and the probe took a bare ordinal. Run b: the probe picks the card BY NAME (`--device-name gfx1201`, the match on the +# newest AMD platform by driver version, the kit worker's dedup rule; `RESULT device_choice name= index= platform= driver= +# cus=`), prints the device's name, CUs and platform on every RESULT line, and refuses with `RESULT error` when no +# gfx1201 is listed; the older-platform duplicate and the gfx1036 run after it by explicit ordinal, as their own labelled +# columns. Run d (17:17Z, the card by name, every variant built) showed a dependent-chain ratio does NOT survive the +# card's own miner: the alu chain read 226 then 195 G steps/s and the ratios swung from 5.9 to 11.8x to 0.17 to 1.3x +# between runs (the loaded card's scheduler, not the ops), while the Mac and 5090 columns were taken with the card alone. +# Run e: the 9070 XT is switched OFF by the RUNNER (`--cards-off amd:gfx1201` at publish, the app's own card path, put +# back on ANY exit with its own enabled flag, identities and cap: rule of 6 October 2026, a script never posts to +# /api/cards) for its three runs; this script only confirms by the process list that the card's opencl worker is gone; +# the 5090 and the 4070 keep mining (NOT --stop-miners). The gfx1036 and the old-platform columns run +# AFTER the restore, beside the miners, as they are not the card under test. mm8 stays exact=unverified: the gfx12 WMMA +# iu8 16x16x16 fragment layout is in no source at hand, and a guessed CPU layout would turn a wrong guess into exact=no. The probe is the family kit's +# family-probe-cl.exe (proto-opencl/family-probe.c of branch ca3-pc1-amd, cross-compiled with mingw, OpenCL.dll at run +# time; the Mac ran the same source bit-exact on Apple OpenCL), three runs per device, each best of 3 with a fresh seed +# per repetition, device event time, bit-exact against the CPU reference on two whole 32-lane groups. Every family is +# tried through every form AMD's OpenCL C offers; a form that does not compile prints build=failed with the first line +# of the build log and the run goes on (run a's finding: AMD's OpenCL C compiles no amd_perm, no sudot4, no WMMA +# builtin; ds_bpermute does compile). Never quits, pauses, resumes or updates the installed app; never writes +# settings.json. Lines: the probe's own `RESULT FAMILY name= ms= gsteps= ratio= exact= path= variant= ... device= cus= +# platform=` and `RESULT FAMILYBEST ...`, each prefixed `RESULT run= dev=