diff --git a/igneum-pow/src/accept.rs b/igneum-pow/src/accept.rs index 709d6f43..fa521aeb 100644 --- a/igneum-pow/src/accept.rs +++ b/igneum-pow/src/accept.rs @@ -9,6 +9,7 @@ //! | (a) static | for every `load`, some instruction between the previous `load` from the same source register and this one, in cyclic order over the 64 instructions, writes that register | //! | (b) static | every register `r0..r7` is the destination of at least one `add`, `sub`, `xor`, `mad`, `shfl` or `load` | //! | (c) dynamic | the program is interpreted for [`ACCEPT_UNITS`] (64) units of 32 lanes at base nonces drawn from SplitMix64 seeded with `FNV-1a-64("igneum-accept/" \|\| seed words as little-endian bytes)`, each `low32(next()) AND NOT 31`, with init words equal to the seed words and the closed-form dataset `dataset_elem(idx, S[0], S[1])` at [`ACCEPT_DATASET_LOG2`] (2^28 words) in place of the memory-hard dataset. Over the 2,048 evaluations: no register has a bit equal in every final value; no load site (iteration, instruction) reads one address in all 32 lanes of any unit; fewer than [`MAX_SATURATED`] (164, 1 percent of 16,384) final register values are 0 or 2^32 - 1; every output bit's ones count is within [`BIAS_TOLERANCE`] (136, 6 sigma) of 1,024; the distinct masked addresses read by one lane in one evaluation, summed over the 2,048 evaluations, exceed [`MIN_DISTINCT_SUM`] (245,760, a mean above 120 of the 128 loads) | +//! | (c''') class v5 | the per-site distinct-index floor of (c'') raised to [`MIN_DISTINCT_RATIO_V5`] (0.995) on the same 2^20-evaluation run, keyed on the state flag; the ratio at the 0.98 floor is still named (c'') first (`docs/design/class-v5-stored-state.md` section 14) | //! //! The dynamic test uses the closed form so that it is a pure function of the program (no cache, no day) and //! costs about a millisecond on one core. A hot-table load (`docs/plans/hot-table.md`) reads the closed form keyed by @@ -40,6 +41,18 @@ pub const MIN_DISTINCT_RATIO_V4: f64 = 0.98; /// Kept for the record and the driver, not wired: the most repeated source value per site over the (c) units' /// 16,384 evaluations (a uniform site repeats a value 2 or 3 times; the finding's bands sit under the ratio instead). pub const MAX_SOURCE_REPEAT_V4: u32 = 8; +/// (c'''), class v5 (`docs/design/class-v5-stored-state.md` section 14): the per-site distinct-index floor of the +/// ratio pass raised from sub-version 3's 0.98 to the bottom of the clean seeds' spread. On the 2^20-evaluation +/// sample the model's spread is about 0.0001 (the collision count is near Poisson with mean n^2 / 2W = 8,192 on +/// the quarter window); the sub-version 3 clean seeds' site rows read 0.9960 at the minimum, 0.9990 at p1 and +/// 1.0000 at the median (adv-accept's census), and the residual class the in-house pass attributed (seed 100767, +/// program 9d68e6286fc817d4: site 6 reads the multiples of 2^19 through `rotl(x * stride, rot)` of a `mad` value +/// that is near zero in a value class, 3.35 percent of its live reads on the top 0.1 percent of items, 1,677 reads +/// of index 0 in 10^6 nonces) reads 0.9919 closed and 0.9920 on the live day at the same sample: 0.012 above the +/// 0.98 floor, 0.003 under this one. The floor is the cheapest fix that reaches the class (main's order of 7 October +/// 2026): no new sample, the verdict at attempt 2 with no 2^24 run; the census test `v5_hot_census` reads its clean +/// rejection rate and the attempts histogram beside the per-site hot-item test. +pub const MIN_DISTINCT_RATIO_V5: f64 = 0.995; /// Hashes the dynamic test evaluates: 2,048. pub const ACCEPT_HASHES: usize = ACCEPT_UNITS * LANES; /// Domain tag of the base-nonce stream. @@ -88,6 +101,11 @@ pub enum Reject { /// `evaluations`, `ratio_milli` / 1000 of a uniform source on its window, under the floor: a low-entropy index band /// (F8's p23, p18, p19, p15, p56). LowEntropySite { site: u8, distinct: u32, evaluations: u32, ratio_milli: u32 }, + /// (c'''), class v5: the load at `site` read `distinct` distinct dataset word indices over `evaluations`, + /// `ratio_milli` / 1000 of the window model, under [`MIN_DISTINCT_RATIO_V5`] (and at or above sub-version 3's + /// floor, else (c'') names it); `top_index` was its most read index, `top_count` times: a hot set from a + /// value-level constant upstream of the site that neither the lineage rule nor the 0.98 floor reaches (seed 100767). + HotItemSite { site: u8, distinct: u32, evaluations: u32, ratio_milli: u32, top_index: u32, top_count: u32 }, /// (c): output bit `bit` was set in `ones` of 2,048 hashes. OutputBias { bit: u8, ones: u32 }, /// (c): the distinct-address sum was `sum`. @@ -108,6 +126,7 @@ impl std::fmt::Display for Reject { Reject::Saturated { count } => write!(f, "(c) {count} of 16384 final register values saturated (limit 163)"), Reject::UnfreshLoadSource { instr, reg } => write!(f, "(a') load at {instr} reads r{reg}, not fresh by dataflow in the loop's steady state (class v4 sub-version 2)"), Reject::RepeatedSource { site, value, count } => write!(f, "(c'') load site {site} read the value {value:#010x} in {count} of 16384 evaluations (limit {})", MAX_SOURCE_REPEAT_V4 - 1), + Reject::HotItemSite { site, distinct, evaluations, ratio_milli, top_index, top_count } => write!(f, "(c''') load site {site} read {distinct} distinct word indices over {evaluations} evaluations, {ratio_milli}/1000 of the window model, under the class v5 floor (most read: index {top_index:#x}, {top_count} times): a hot set from a value-level constant upstream"), Reject::LowEntropySite { site, distinct, evaluations, ratio_milli } => write!(f, "(c'') load site {site} read {distinct} distinct word indices over {evaluations} evaluations, {}.{:03} of a uniform source on its window (floor {MIN_DISTINCT_RATIO_V4} at 2^20)", ratio_milli / 1000, ratio_milli % 1000), Reject::SaturatedSource { site, count } => write!(f, "(c') load site {site} read a saturated source value in {count} of 16384 evaluations (limit 163)"), Reject::OutputBias { bit, ones } => write!(f, "(c) output bit {bit} set in {ones} of 2048 hashes"), @@ -195,31 +214,37 @@ pub fn check_distinct_indices_v4(p: &Program) -> Result<(), Reject> { /// window (`N - N^2 / 2W`, the window `2^28 >> min(win, 2)` words of the closed-form dataset), `Err` at the first /// site under `floor`, else the minimum ratio and its site. pub fn distinct_ratio_pass(p: &Program, units: usize, floor: f64) -> Result<(f64, usize), Reject> { - let n = (units * LANES * ITERATIONS) as f64; - let d = distinct_indices_v4(p, units)?; - let mut min = (f64::MAX, 0usize); - let mut site = 0usize; - for i in &p.instrs { - if !i.op.is_load() { - continue; - } - let wsize = ((1u64 << ACCEPT_DATASET_LOG2) >> (i.win as u64).min(2)) as f64; - let ratio = d[site] as f64 / (n - n * n / (2.0 * wsize)); - if ratio < floor { - return Err(Reject::LowEntropySite { site: site as u8, distinct: d[site], evaluations: n as u32, ratio_milli: (ratio * 1000.0) as u32 }); - } - if ratio < min.0 { - min = (ratio, site); - } - site += 1; - } - Ok(min) + let stats = site_index_stats(p, units)?; + distinct_ratio_on(p, &stats, units, floor) } /// The distinct dataset word indices every load site reads over `units` units of the seed's acceptance stream on /// the closed-form words (the sample caps the count near `units x 32 x 8`, so a site's index entropy is read only /// below about log2 of that). pub fn distinct_indices_v4(p: &Program, units: usize) -> Result, Reject> { + Ok(site_index_stats(p, units)?.into_iter().map(|s| s.distinct).collect()) +} + +/// What one load site's word indices over the ratio pass look like: the distinct count (c'' and c'''), the sum of +/// `C(run, 2)` over the indices (the colliding pairs), and the most read index with its count (the diagnostic the +/// reject names). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SiteIndexStats { + pub distinct: u32, + pub pairs: u64, + pub top_index: u32, + pub top_count: u32, +} + +/// The window of a load site in words at the rule's dataset: `2^28 >> min(win, 2)`. +pub fn site_window_words(ins: &Instr) -> u64 { + (1u64 << ACCEPT_DATASET_LOG2) >> (ins.win as u64).min(2) +} + +/// One interpreter run over `units` units with every load site's word indices kept, then per site the sorted +/// run lengths: [`SiteIndexStats`] per site in load order. The ratio pass (c'') and the hot-item rule (c''') read +/// the same run, so class v5 pays the sort once. +pub fn site_index_stats(p: &Program, units: usize) -> Result, Reject> { let loads = p.loads_per_hash(); let sites = loads / ITERATIONS; let mut acc = Acc { @@ -239,12 +264,89 @@ pub fn distinct_indices_v4(p: &Program, units: usize) -> Result, Reject let mut out = Vec::with_capacity(sites); for ix in acc.indices.take().unwrap().iter_mut() { ix.sort_unstable(); - ix.dedup(); - out.push(ix.len() as u32); + let mut st = SiteIndexStats { distinct: 0, pairs: 0, top_index: 0, top_count: 0 }; + let mut i = 0; + while i < ix.len() { + let mut j = i + 1; + while j < ix.len() && ix[j] == ix[i] { + j += 1; + } + let run = (j - i) as u32; + st.distinct += 1; + st.pairs += (run as u64) * (run as u64 - 1) / 2; + if run > st.top_count { + st.top_count = run; + st.top_index = ix[i]; + } + i = j; + } + out.push(st); } Ok(out) } +/// (c'''), class v5: the raised floor on the ratio pass's sample, `Err` at the first load site under `floor`, else +/// the minimum ratio and its site. Pure in the program as (c'') is (the closed form, the seed's acceptance stream); +/// the state leaves move the dataset's words, not the indices a site reads (adv-accept read the exemplar's site 6 +/// at 0.9919 closed and 0.9920 on the live day). +pub fn hot_item_pass(p: &Program, stats: &[SiteIndexStats], units: usize, floor: f64) -> Result<(f64, usize), Reject> { + let n = (units * LANES * ITERATIONS) as f64; + let mut min = (f64::MAX, 0usize); + let mut site = 0usize; + for i in &p.instrs { + if !i.op.is_load() { + continue; + } + let st = &stats[site]; + let ratio = site_ratio(st.distinct, n, site_window_words(i)); + if ratio < floor { + return Err(Reject::HotItemSite { site: site as u8, distinct: st.distinct, evaluations: n as u32, ratio_milli: (ratio * 1000.0) as u32, top_index: st.top_index, top_count: st.top_count }); + } + if ratio < min.0 { + min = (ratio, site); + } + site += 1; + } + Ok(min) +} + +/// A site's distinct-index ratio against the window model `N - N^2 / 2W` (the (c'') form, kept so the census +/// numbers of sub-version 3 read on the same scale). +pub fn site_ratio(distinct: u32, n: f64, window: u64) -> f64 { + distinct as f64 / (n - n * n / (2.0 * window as f64)) +} + +/// The ratio floor (c'') on precomputed site stats: `Err` at the first site under `floor`, else the minimum ratio +/// and its site (the pass [`distinct_ratio_pass`] runs when it has no stats yet). +pub fn distinct_ratio_on(p: &Program, stats: &[SiteIndexStats], units: usize, floor: f64) -> Result<(f64, usize), Reject> { + let n = (units * LANES * ITERATIONS) as f64; + let mut min = (f64::MAX, 0usize); + let mut site = 0usize; + for i in &p.instrs { + if !i.op.is_load() { + continue; + } + let d = stats[site].distinct; + let ratio = site_ratio(d, n, site_window_words(i)); + if ratio < floor { + return Err(Reject::LowEntropySite { site: site as u8, distinct: d, evaluations: n as u32, ratio_milli: (ratio * 1000.0) as u32 }); + } + if ratio < min.0 { + min = (ratio, site); + } + site += 1; + } + Ok(min) +} + +/// (c'') then (c'''), class v5: one 2^20-evaluation run, the ratio floor of sub-version 3 and the hot-item rule on +/// it. The order is the module table's: a low-entropy band is named before a hot item on the same site. +pub fn check_indices_v5(p: &Program) -> Result<(), Reject> { + let stats = site_index_stats(p, ACCEPT_UNITS_DISTINCT_V4)?; + distinct_ratio_on(p, &stats, ACCEPT_UNITS_DISTINCT_V4, MIN_DISTINCT_RATIO_V4)?; + hot_item_pass(p, &stats, ACCEPT_UNITS_DISTINCT_V4, MIN_DISTINCT_RATIO_V5).map(|_| ()) +} + /// Whether `class` is the class v4 shape (the 256-instruction shadow block over the class v3 base, the pass count and /// the era set aside): the shape the sub-version 2 rules (a') and (c') apply to, on every draw path. pub fn is_class_v4_shape(class: &LoadClass) -> bool { @@ -601,8 +703,14 @@ pub fn check_dynamic(p: &Program) -> Result { if let Some((site, &count)) = acc.sat_source.iter().enumerate().find(|(_, &c)| c >= MAX_SATURATED) { return Err(Reject::SaturatedSource { site: site as u8, count }); } - // (c''), the ratio on the candidate that passed everything else (the draw's last and dearest test) - check_distinct_indices_v4(p)?; + // (c''), the ratio on the candidate that passed everything else (the draw's last and dearest test); class v5 + // reads (c''') the hot-item rule on the same run (`docs/design/class-v5-stored-state.md` section 14), keyed + // on the state flag so no class v4 verdict moves + if p.class.state { + check_indices_v5(p)?; + } else { + check_distinct_indices_v4(p)?; + } } let half = (ACCEPT_HASHES / 2) as u32; let mut bias_max = 0u32; @@ -631,6 +739,147 @@ mod tests { use crate::generator::{candidate, candidate_class, generate, generate_class, GeneratorConfig, generate_v1, LoadClass}; use crate::verify::{DatasetMode, DatasetSource}; + /// The f8 label space's chain draw (adv-accept's census space): epoch and era seed bytes for seed `k`, and the + /// candidate at `attempt` under `base` (the chain's class v4 sub-version 3 draw for `V4_CLASS`, the class v5 draw + /// for `V5_CLASS`), stamped as `generate_era` stamps it. + fn f8_seed(k: u32) -> (Vec, Vec) { + use crate::seed::seed_words_from_bytes; + let w = |s: String| -> Vec { seed_words_from_bytes(s.as_bytes()).iter().flat_map(|x| x.to_le_bytes()).collect() }; + (w(format!("igneum-attack-f8/program/{k}")), w(format!("igneum-attack-f8/era/{k}"))) + } + fn f8_label(epoch: &[u8]) -> String { + format!("igneum-epoch/{}", epoch.iter().map(|b| format!("{b:02x}")).collect::()) + } + fn f8_candidate(epoch: &[u8], era: &[u8], attempt: u32, base: LoadClass) -> Program { + use crate::generator::{era_generator_of, V3_ALLOWED}; + let mut p = candidate_class(&f8_label(epoch), epoch, attempt, LoadClass::era(base, era, &V3_ALLOWED)); + p.generator = era_generator_of(&base); + p.era_bytes = Some(era.to_vec()); + p + } + fn f8_draw(epoch: &[u8], era: &[u8], base: LoadClass) -> Program { + use crate::generator::{generate_era, V3_ALLOWED}; + generate_era(&f8_label(epoch), epoch, base, era, &V3_ALLOWED) + } + + /// (c''') known-failed first (`docs/design/class-v5-stored-state.md` section 14; main's order of 7 October + /// 2026): adv-accept's seed 100767 is the chain's class v4 sub-version 3 draw at attempt 2 (program + /// 9d68e6286fc817d4), passes every part of the sub-version 3 rule with its site 6 at 0.9919, and reads a hot set + /// live (3.35 percent of site 6's reads on the top 0.1 percent of items, the multiples of 2^19). The same shape + /// under class v5 is refused at attempt 2 by the raised floor, naming site 6 under 0.995, and the class v5 draw + /// moves past it; the class v4 verdict does not move. A clean seed (the genesis draw of both classes) clears + /// the floor with room. + #[test] + fn class_v5_hot_set_rule_known_failed_seed_100767() { + use crate::generator::V5_CLASS; + let (epoch, era) = f8_seed(100767); + let v4 = f8_candidate(&epoch, &era, 2, V4_CLASS); + assert_eq!(format!("{:016x}", v4.program_id()), "9d68e6286fc817d4", "the exemplar is the chain's sub-version 3 program"); + assert_eq!(v4.seed, [0xcc5466bf, 0x375211d6, 0xf22b6e63, 0x638d9f8b, 0x85903c90, 0x03605228, 0x5f97734b, 0x910fb493]); + assert!(check(&v4).is_ok(), "class v4 sub-version 3 accepts it: {:?}", check(&v4).err()); + let v4_stats = site_index_stats(&v4, ACCEPT_UNITS_DISTINCT_V4).unwrap(); + let (min, site) = distinct_ratio_on(&v4, &v4_stats, ACCEPT_UNITS_DISTINCT_V4, 0.0).unwrap(); + println!("seed 100767 class v4: min site {site} ratio {min:.4} (distinct {}, most read index {:#x} {} times)", v4_stats[site].distinct, v4_stats[site].top_index, v4_stats[site].top_count); + assert_eq!(site, 6); + assert!(min > 0.98 && min < 0.995, "the exemplar sits between the floors: {min:.4}"); + + let v5 = f8_candidate(&epoch, &era, 2, V5_CLASS); + assert_eq!(v5.instrs, v4.instrs, "the same base program under class v5"); + match check(&v5) { + Err(Reject::HotItemSite { site, ratio_milli, distinct, evaluations, top_index, top_count }) => { + println!("seed 100767 class v5 attempt 2: (c''') site {site} {distinct} of {evaluations}, ratio {}.{:03}, most read {top_index:#x} x{top_count}", ratio_milli / 1000, ratio_milli % 1000); + assert_eq!(site, 6); + assert!(ratio_milli >= 980 && ratio_milli < 995); + } + other => panic!("class v5 must refuse the exemplar at attempt 2 by (c'''), got {other:?}"), + } + let drawn = f8_draw(&epoch, &era, V5_CLASS); + println!("seed 100767 class v5 draw: attempt {} id {:016x}", drawn.attempt, drawn.program_id()); + assert!(drawn.attempt > 2, "the class v5 draw moves past the exemplar"); + assert!(check(&drawn).is_ok()); + let drawn_v4 = f8_draw(&epoch, &era, V4_CLASS); + assert_eq!(drawn_v4.attempt, 2, "the class v4 draw still lands on it"); + + for (name, p) in [("genesis v4", generate_class("igneum-genesis", V4_CLASS)), ("genesis v5", generate_class("igneum-genesis", V5_CLASS))] { + let st = site_index_stats(&p, ACCEPT_UNITS_DISTINCT_V4).unwrap(); + let (min, site) = distinct_ratio_on(&p, &st, ACCEPT_UNITS_DISTINCT_V4, 0.0).unwrap(); + println!("{name}: min site {site} ratio {min:.4}"); + assert!(min >= MIN_DISTINCT_RATIO_V5, "{name}: a clean seed clears the class v5 floor: {min:.4}"); + } + } + + /// The census behind the floor (run by hand on a box: `cargo test --release --lib -- --ignored v5_hot_census + /// --nocapture`, `IGNEUM_V5_CENSUS_SEEDS` seeds of the f8 label space from `IGNEUM_V5_CENSUS_FROM`, default + /// 4,600 from 0, `IGNEUM_V5_CENSUS_THREADS` threads, default 88): per seed the class v4 sub-version 3 draw's + /// attempt and its accepted program's minimum site ratio, the class v5 draw's attempt, and the counts under + /// candidate floors, so the clean rejection rate and the attempts histogram of the chosen floor are on record. + #[test] + #[ignore] + fn v5_hot_census() { + use crate::generator::V5_CLASS; + use std::sync::atomic::{AtomicU32, Ordering}; + let seeds: u32 = std::env::var("IGNEUM_V5_CENSUS_SEEDS").ok().and_then(|v| v.parse().ok()).unwrap_or(4600); + let from: u32 = std::env::var("IGNEUM_V5_CENSUS_FROM").ok().and_then(|v| v.parse().ok()).unwrap_or(0); + let threads: usize = std::env::var("IGNEUM_V5_CENSUS_THREADS").ok().and_then(|v| v.parse().ok()).unwrap_or(88); + let next = AtomicU32::new(from); + let t0 = std::time::Instant::now(); + // per seed: (k, v4 attempt, v4 min ratio, v4 min site, v5 attempt, v5 min ratio) + let rows: Vec<(u32, u32, f64, usize, u32, f64)> = std::thread::scope(|sc| { + let hs: Vec<_> = (0..threads).map(|_| sc.spawn(|| { + let mut out = Vec::new(); + loop { + let k = next.fetch_add(1, Ordering::Relaxed); + if k >= from + seeds { + break out; + } + let (epoch, era) = f8_seed(k); + let v4 = f8_draw(&epoch, &era, V4_CLASS); + let st = site_index_stats(&v4, ACCEPT_UNITS_DISTINCT_V4).unwrap(); + let (min4, site4) = distinct_ratio_on(&v4, &st, ACCEPT_UNITS_DISTINCT_V4, 0.0).unwrap(); + let v5 = f8_draw(&epoch, &era, V5_CLASS); + let st5 = site_index_stats(&v5, ACCEPT_UNITS_DISTINCT_V4).unwrap(); + let (min5, _) = distinct_ratio_on(&v5, &st5, ACCEPT_UNITS_DISTINCT_V4, 0.0).unwrap(); + out.push((k, v4.attempt, min4, site4, v5.attempt, min5)); + } + })).collect(); + let mut rows: Vec<_> = hs.into_iter().flat_map(|h| h.join().unwrap()).collect(); + rows.sort_by_key(|r| r.0); + rows + }); + let n = rows.len() as f64; + let mut sorted: Vec = rows.iter().map(|r| r.2).collect(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let q = |p: f64| sorted[((p * (sorted.len() - 1) as f64).round() as usize).min(sorted.len() - 1)]; + println!("v5_hot_census: {} seeds {from}..{} in {:.0} s on {threads} threads", rows.len(), from + seeds, t0.elapsed().as_secs_f64()); + println!("class v4 sub-version 3 accepted programs' minimum site ratio: min {:.4} p0.1 {:.4} p1 {:.4} p5 {:.4} median {:.4} max {:.4}", sorted[0], q(0.001), q(0.01), q(0.05), q(0.5), sorted[sorted.len() - 1]); + for floor in [0.98, 0.99, 0.995, 0.998, 0.999] { + let under = rows.iter().filter(|r| r.2 < floor).count(); + println!("floor {floor:.3}: {under} of {} class v4 accepted programs under it ({:.3} percent)", rows.len(), under as f64 * 100.0 / n); + } + let mut lowest: Vec<_> = rows.iter().collect(); + lowest.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap()); + for r in lowest.iter().take(25) { + println!("low: seed {} v4 attempt {} min site {} ratio {:.4}; v5 attempt {} min ratio {:.4}", r.0, r.1, r.3, r.2, r.4, r.5); + } + let hist = |pick: fn(&(u32, u32, f64, usize, u32, f64)) -> u32| { + let mut h = std::collections::BTreeMap::new(); + for r in &rows { + *h.entry(pick(r)).or_insert(0u32) += 1; + } + let mean = rows.iter().map(|r| pick(r) as f64).sum::() / n; + (h, mean) + }; + let (h4, m4) = hist(|r| r.1); + let (h5, m5) = hist(|r| r.4); + println!("attempts histogram class v4 (mean {m4:.3}): {:?}", h4); + println!("attempts histogram class v5 (mean {m5:.3}): {:?}", h5); + let moved = rows.iter().filter(|r| r.1 != r.4).count(); + println!("seeds whose class v5 draw lands on another attempt than the class v4 draw: {moved} of {} ({:.3} percent)", rows.len(), moved as f64 * 100.0 / n); + let v5_under = rows.iter().filter(|r| r.5 < MIN_DISTINCT_RATIO_V5).count(); + println!("class v5 accepted programs under the class v5 floor {MIN_DISTINCT_RATIO_V5}: {v5_under} (must be 0)"); + assert_eq!(v5_under, 0); + } + #[test] fn distinct_bound_scales_with_the_load_count() { assert_eq!(min_distinct_sum(128), MIN_DISTINCT_SUM); diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs index 46955d6c..2e2f2426 100644 --- a/igneum-pow/src/generator.rs +++ b/igneum-pow/src/generator.rs @@ -2088,6 +2088,12 @@ mod tests { let first = candidate_class(seed, seed.as_bytes(), v4.attempt, v4.class); assert!(shadow_removable_count(&first.shadow) * 1000 > first.shadow.len() * SHADOW_REMOVABLE_MAX_PERMILLE || v5.attempt != v4.attempt, "{seed}: v5 differs from v4 without a redraw"); } + if v5.attempt != v4.attempt { + // the attempt moved: the v4 draw's program under the v5 shape is refused by a class v5 rule ((c''') the + // raised floor, or the acceptance reading a redrawn shadow), never silently + let same = candidate_class(seed, seed.as_bytes(), v4.attempt, v5.class); + assert!(crate::accept::check(&same).is_err(), "{seed}: the v5 draw skipped attempt {} without a reason", v4.attempt); + } // the draw equality above is the claim; sub-version 3's own freshness rule (dataflow, with the last resort after // the 256 cap) is what the chain applies, so the sub-version 1 scan below is informational for v5 let _ = scan(&v5);