From 017e70376489251e18564c0abce7e466e606c8b3 Mon Sep 17 00:00:00 2001 From: igneum-josh <337424239+igneum-josh@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:44:55 +0100 Subject: [PATCH] Counter ASIC 3.0 gates (hash): class v4 sub-version 3, second commit (AP-F8-1): the shared-operand rule in the draw and (a'), rule (c'') the distinct-index ratio at 0.98 over 2^20, the known-failed test on the chain's candidates The shared-operand rule (or-then-xor, or-then-sub, xor-then-or on one operand is a mask) in the draw's source rule and in the acceptance's (a') pass: p23's chain attempt 1 (id d65122675f16a1c7) now draws site 7 from r5 and passes. Rule (c''): over 4,096 units (2^20 evaluations per site, the shadow executed) every load site's distinct word indices against the uniform expectation on its window must reach 0.98, the last test of the chosen candidate; the floor sits 0.015 from the clean minimum (0.9960) and from the strong failing maximum (p56 0.9654). The staged 2^24 pass was not taken: at 2^24 the clean p44 and p52 (0.9612, 0.9613) read the weak four's value (0.9181 to 0.9630), so no 2^24 floor separates them. Open tail: p4, p8, p10, p34 (0.9927 to 0.9963 at 2^20), unattributed and chased. The (B) bound stays unwired. The test class_v4_distinct_ratio_rejects_the_low_entropy_band pins p15 (attempt 3), p18 (2), p19 (0), p56 (2) refused and p23's attempt 1 passing, its r6 variant refused by (a') and by the ratio (0.836). The stream is unchanged: re-export diff 0 on the eight packs, id a785001687d8688a, PROGRAM_SUBVERSION_V4 stays 3, fingerprints and zip sha256 stand. Cost: one 2^20 pass per chosen candidate, 2.1 to 2.2 s on one box-2 core. Co-Authored-By: Claude Fable 5.1 --- docs/fud-ledger.md | 2 + igneum-pow/src/accept.rs | 120 +++++++++++++++--------- igneum-pow/src/generator.rs | 180 ++++++++++++++++++++++++++++-------- 3 files changed, 216 insertions(+), 86 deletions(-) diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index 0e6c0490e..65364d8c3 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -2456,4 +2456,6 @@ AP-F8-2, the exhaustion half, FIXED-AND-PASSED at 8bdcbdd8 on the attack-pass la Sub-version 2's hot-set half did not read green: F8's 64-seed gate at 39 of 64 had 8 over 1.2x of the window model (p23 4.82x, p19 3.32x, p15 2.57x, p18 2.50x, p34 1.25x; p4, p8, p10 unattributed at 1.22x to 1.50x). AP-F8-3 (7 October 2026, 14:0x UTC, the hash lane): the cause of the whole residual is that `accept.rs` never executed the latency-shadow block. Its interpreter (`run_unit`) was written for class v2 and v3 and ran the 64 base instructions per iteration and nothing after instruction 63, while the hash (`verify.rs`, the kernels) runs the shadow 27 times at the end of every iteration; so every dynamic acceptance test (c), (c') judged a class v4 program the chain never hashes. Main's word (14:1x UTC): 0.3.21 ships object byte 5 (sub-version 1); sub-version 3 is 0.3.22's and starts with this fix. Sub-version 3, first commit: `run_unit` executes the shadow block after instruction 63 of every iteration, `reps` times with the iteration's sel, as the hash does; the test `acceptance_executes_the_shadow_block_as_the_verifier_does` pins the acceptance's execution to `verify.rs` on the devnet epoch-0 program and the six test eras (the output bit counts over the 64 units equal, and different with the shadow stripped), so the two paths cannot diverge silently again; `PROGRAM_SUBVERSION_V4` = 3 (a new acceptance verdict is a new stream); the devnet epoch-0 seed still accepts at attempt 1, so its program and fingerprint are sub-version 2's (e370fb2080b7dbb1) under the new id a785001687d8688a (the must-differ set: c120d7963abdcd96, 1a4230699a6b9c60, a788661687db4bb3); the packs zip sha256 4f2445c50c58d76a5544023492d8b858d0b07c5e372d31f9c90c4ce51f829154. The class behind p23, localised from its program and reproduced in the acceptance's own execution: site 7 (instruction 38) reads r6 after 25 `mulhi r6`, 31 `or r6 |= r4`, 35 `xor r6 ^= r4`, which is `r6 & ~r4`, an AND mask the lineage rule counted as fresh because the xor's operand is the or's; over 2^20 evaluations on the closed-form words site 7 reads 874,953 distinct word indices against about 1,046,500 for every other site (0.84 of uniform; 2.2 s on one core), over 2^24 8,979,203 against about 16,260,000 (0.55; 35 s). The second sub-version 3 commit (held, prepared in the worktree) is a per-site distinct-index ratio against the uniform expectation of the site's window, its threshold set from the clean seeds' spread and its sample size from the cost line above; the dynamic bounds as first specified (a most-repeated-value bound at 16,384 and a distinct floor at 2^19.5 over 2^20) do not reach p23 and are not committed. The crate suite at ddacfbd3 on box 2 (route "box 2 for class suite, priority normal", rc 0, 41 s, 14:20Z): 63 lib + 7 derive + 4 mixer + 19 packs + 2 recheck + 7 scratch = 102 passed, 0 failed, the agreement test included. F8's final 64-seed table on sub-version 2 (the attack-pass lane, 14:2x UTC): 9 over 1.2x (p23 4.82x, p19 3.32x, p15 2.57x, p18 2.50x, p56 2.01x, p10 1.50x, p8 1.38x, p34 1.25x, p4 1.22x), the 55 clean seeds at 0.9915x to 1.144x; the 256-item bucket entropy over the window separates the strong four only (0.637 to 0.974 against a clean minimum of 0.9865 over 848 site rows), so the threshold of the second commit's distinct-index ratio comes from a run of that ratio on the 55 clean seeds at 2^20. The static census at ddacfbd3 (box 2, 14:22Z, 4,096 chain-shaped seeds plus F8's p1 to p3): 4,099 programs, 0 lossy-sourced load sites of 65,584, 0 exhaustions, the accepted attempt geometric as before (1,328 at attempt 0, 917, 622, 409, 284, ... one each at 16 and 17; mean 1.998, max 17), so the shadow-executed verdicts move a handful of seeds' attempts and nothing else; the devnet epoch-0 seed at attempt 1, id a785001687d8688a. +Sub-version 3, second commit (7 October 2026, 14:44 UTC, the hash lane; the sub-version number stays 3 and the stream is unchanged: re-export diff 0 on the eight packs, the id a785001687d8688a, the seven fingerprints and the zip sha256 stand), two rules. The shared-operand rule, in the draw's source rule and in the acceptance's (a') pass: or-then-xor or or-then-sub on one operand is `d & ~s`, xor-then-or is `d | s`, so the second write leaves the register lossy although either op alone injects; any write to either register clears the relation. On p23 the chain's attempt 1 (id d65122675f16a1c7) now draws site 7 (instruction 38) from r5 and passes; the devnet epoch-0 seed still draws attempt 1, the same program. Rule (c''), the distinct-index ratio: over 4,096 units (2^20 evaluations per site, the closed-form words, the shadow executed) every load site's count of distinct word indices against the uniform expectation on its window (N - N^2 / 2W, the window 2^28 >> min(win, 2)) must reach 0.98 (`MIN_DISTINCT_RATIO_V4`), the last test of the chosen candidate; a candidate under it is rejected and the next attempt drawn under the 256 cap and the last resort. The floor from the 64-seed run at 2^20 (box 2): the 55 clean seeds' minimum over their site rows 0.9960 (p1 0.9990, median 1.0000); the strong five p23 0.8361, p18 0.9274, p19 0.9335, p15 0.9432, p56 0.9654; 0.98 sits 0.015 from each side. The chain's own candidates it refuses (the test `class_v4_distinct_ratio_rejects_the_low_entropy_band`, box 2, 2.1 to 2.2 s each): p15 attempt 3 id 52638ea2e8b0fd68 site 2 at 0.943, p18 attempt 2 id 9a37e9489d8ba698 site 6 at 0.927, p19 attempt 0 id 79d7441de0689223 site 15 at 0.933, p56 attempt 2 id 486a8ad2701ec3b5 site 2 at 0.965; p23's attempt 1 with site 7 put back to r6 is refused by (a') (`UnfreshLoadSource`) and, run anyway, by the ratio at 0.836 (874,928 distinct of 1,048,576). Main's rule for a staged 2^24 pass (taken only if 2^24 separates the weak four from the clean seeds by at least the 2^20 gap) was decided by the 2^24 lines (box 2, 35 s per seed on one core): the weak four p34 0.9181, p4 0.9614, p8 0.9630, p10 0.9612; the clean seeds p44 0.9612, p52 0.9613, p3 0.9971, p2 and p5 1.0004; p23's attempt 1 1.0004. Two clean seeds sit on the weak four's value, so a 2^24 floor that reaches the weak four rejects clean seeds, and the 0.961 that recurs on both sides is a band the ratio reads at 2^24 that F8's hot-set gate did not flag on p44 or p52. Committed: the ratio at 0.98 over 2^20 alone (`ACCEPT_UNITS_DISTINCT_V4` = 4096), no 2^24 stage. Open tail: p4, p8, p10 and p34 (1.22x to 1.50x on F8's gate) read 0.9927 to 0.9963 at 2^20, inside the clean spread; unattributed and chased. The (B) most-repeated-value bound stays in the file unwired (`MAX_SOURCE_REPEAT_V4`, `most_repeated`). Cost: the chosen candidate's acceptance gains one 2^20 pass, 2.1 to 2.2 s on one box-2 core, once per epoch draw per node. + Owed (recorded, not run, by Josh'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. diff --git a/igneum-pow/src/accept.rs b/igneum-pow/src/accept.rs index 78306911d..5777be4b6 100644 --- a/igneum-pow/src/accept.rs +++ b/igneum-pow/src/accept.rs @@ -23,19 +23,22 @@ use crate::verify::{dataset_elem, fold_words, load_index, splitmix32, ScratchMod /// Units (32-lane warps) the dynamic test interprets. pub const ACCEPT_UNITS: usize = 64; -/// Class v4 sub-version 3 (AP-F8-1's low-entropy-band class, 7 October 2026, the attack-pass gate's numbers through -/// main): rule (c''), two parts, both keyed on the class v4 shape. (A) The distinct-index bound: over -/// [`ACCEPT_UNITS_DISTINCT_V4`] units (2^20 evaluations per site) the count of DISTINCT dataset indices a load site -/// reads must be at least [`MIN_DISTINCT_INDICES_V4`] (2^19.5): a site with k bits of index entropy reads about 2^k -/// distinct, and the gate's 1.2x at the top 0.1 percent corresponds to about 18.5 bits (k = 12 reads about 45x, 15 -/// 6.7x, 17 2.4x, 18 about 1.2x). (B) The most-repeated-value bound: over the (c) units' 16,384 evaluations no load -/// site reads one source value [`MAX_SOURCE_REPEAT_V4`] times or more (a single item trips the gate alone at about -/// 78 repeats per 16,384; a uniform source repeats at most 2 or 3). A candidate failing either is rejected and the -/// next attempt drawn under the 256 cap and the last resort. +/// Class v4 sub-version 3, rule (c''): the per-site distinct-index RATIO (AP-F8-1's low-entropy-band class, 7 October +/// 2026, the attack-pass gate's numbers through main), keyed on the class v4 shape. Over [`ACCEPT_UNITS_DISTINCT_V4`] +/// units (2^20 evaluations per site) the count of distinct dataset word indices a load site reads, against the +/// expectation of a uniform source on the site's window (N - N^2 / 2W), must reach [`MIN_DISTINCT_RATIO_V4`]. The +/// floor sits between the 55 clean F8 seeds' minimum over their site rows (0.9960; the clean p1 0.9990, the median +/// 1.0000) and the strong failing seeds' maximum (p56 0.9654; p23 0.8361, p18 0.9274, p19 0.9335, p15 0.9432), +/// 0.015 from each. The open tail: F8's p4, p8, p10 and p34 (1.22x to 1.50x on the gate) read 0.9927 to 0.9963 at +/// 2^20, inside the clean spread, and a 2^24 pass does not separate them either (p34 0.9181, p4 0.9614, p8 0.9630, +/// p10 0.9612 against the clean p44 0.9612, p52 0.9613, p3 0.9971, p2 and p5 1.0004); they stay unattributed and +/// chased in `docs/fud-ledger.md` AP-F8-1. A candidate under the floor is rejected and the next attempt drawn under +/// the 256 cap and the last resort. Measured on one box-2 core with the shadow executed: 2.8 s per chosen candidate. pub const ACCEPT_UNITS_DISTINCT_V4: usize = 4096; -/// (A): the floor on distinct indices per site over 2^20 evaluations, 2^19.5 rounded. -pub const MIN_DISTINCT_INDICES_V4: u32 = 741_455; -/// (B): the most repeated source value per site over the (c) units' 16,384 evaluations is rejected at this count. +/// The ratio floor at 2^20. +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; /// Hashes the dynamic test evaluates: 2,048. pub const ACCEPT_HASHES: usize = ACCEPT_UNITS * LANES; @@ -81,9 +84,10 @@ pub enum Reject { /// (c'') (B), class v4 sub-version 3: the load at `site` read the value `value` in `count` of its 16,384 (c) /// evaluations (limit [`MAX_SOURCE_REPEAT_V4`] - 1): one constant upstream that the lineage rule cannot see. RepeatedSource { site: u8, value: u32, count: u32 }, - /// (c'') (A), class v4 sub-version 3: the load at `site` read only `distinct` distinct dataset indices over 2^20 - /// evaluations (floor [`MIN_DISTINCT_INDICES_V4`]): a low-entropy index band (F8's p23, p15, p18, p19). - LowEntropySite { site: u8, distinct: u32 }, + /// (c''), class v4 sub-version 3: the load at `site` read `distinct` distinct dataset word indices over + /// `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): output bit `bit` was set in `ones` of 2,048 hashes. OutputBias { bit: u8, ones: u32 }, /// (c): the distinct-address sum was `sum`. @@ -104,7 +108,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::LowEntropySite { site, distinct } => write!(f, "(c'') load site {site} read {distinct} distinct indices over {} evaluations (floor {})", ACCEPT_UNITS_DISTINCT_V4 * LANES * ITERATIONS, MIN_DISTINCT_INDICES_V4), + 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"), Reject::DistinctAddresses { sum } => { @@ -167,7 +171,8 @@ fn check_injecting_writes(instrs: &[Instr]) -> Result<(), Reject> { Ok(()) } -/// The most repeated value of `values` (sorted in place) and that value. +/// The most repeated value of `values` (sorted in place) and that value: (B), kept for the driver, not wired. +#[allow(dead_code)] fn most_repeated(values: &mut [u32]) -> (u32, u32) { values.sort_unstable(); let (mut best, mut best_v, mut run) = (0u32, 0u32, 0u32); @@ -181,17 +186,34 @@ fn most_repeated(values: &mut [u32]) -> (u32, u32) { (best, best_v) } -/// (c'') (A), class v4 sub-version 3: the program interpreted for [`ACCEPT_UNITS_DISTINCT_V4`] units on the seed's -/// acceptance stream (the (c) units first) with every load's dataset index recorded per site; a site reading fewer -/// than [`MIN_DISTINCT_INDICES_V4`] distinct indices over its 2^20 evaluations is a low-entropy band (a constant or a -/// forced equality upstream that the lineage rule cannot see) and the candidate is rejected. +/// (c''), class v4 sub-version 3: the 2^20 ratio pass on the chosen candidate (the constants above). pub fn check_distinct_indices_v4(p: &Program) -> Result<(), Reject> { - for (site, &distinct) in distinct_indices_v4(p, ACCEPT_UNITS_DISTINCT_V4)?.iter().enumerate() { - if distinct < MIN_DISTINCT_INDICES_V4 { - return Err(Reject::LowEntropySite { site: site as u8, distinct }); + distinct_ratio_pass(p, ACCEPT_UNITS_DISTINCT_V4, MIN_DISTINCT_RATIO_V4).map(|_| ()) +} + +/// One ratio pass over `units`: every load site's distinct word indices against the uniform expectation on its +/// 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(()) + Ok(min) } /// The distinct dataset word indices every load site reads over `units` units of the seed's acceptance stream on @@ -235,18 +257,32 @@ pub fn is_class_v4_shape(class: &LoadClass) -> bool { /// `docs/analysis/ca3-v4-uniform.md`): a load leaves its destination fresh only if its source was (a saturated /// source reads one fixed word); add, sub, xor, mad and shfl if either operand was; rotl and rotr if the operand /// was (a rotate maps all-ones and zero to themselves); or, mul and mulhi never. -fn freshness_pass(p: &Program, fresh: &mut [bool; 8], check: bool) -> Result<(), Reject> { +fn freshness_pass(p: &Program, fresh: &mut [bool; 8], pair_op: &mut [Option<(Op, usize)>; 8], check: bool) -> Result<(), Reject> { for (k, i) in p.instrs.iter().chain(p.shadow.iter()).enumerate() { let (d, a) = (i.dst as usize, i.src as usize); if check && i.op.is_load() && !fresh[a] { return Err(Reject::UnfreshLoadSource { instr: k as u8, reg: i.src }); } - fresh[d] = match i.op { - Op::Load | Op::WLoad | Op::Scratch | Op::Hot => fresh[a], - Op::Add | Op::Sub | Op::Xor | Op::Mad | Op::Shfl => fresh[d] || fresh[a], - Op::Rotl | Op::Rotr => fresh[d], - Op::Or | Op::Mul | Op::MulHi => false, - }; + // the shared-operand idiom (sub-version 3): or-then-xor or or-then-sub on one operand is `d & ~s`, xor-then-or + // is `d | s`: lossy, though the second op would inject on its own (F8's p23: `or r6 |= r4; xor r6 ^= r4`) + let masked = matches!((pair_op[d], i.op), (Some((Op::Or, s)), Op::Xor) | (Some((Op::Or, s)), Op::Sub) | (Some((Op::Xor, s)), Op::Or) if s == a); + fresh[d] = !masked + && match i.op { + Op::Load | Op::WLoad | Op::Scratch | Op::Hot => fresh[a], + Op::Add | Op::Sub | Op::Xor | Op::Mad | Op::Shfl => fresh[d] || fresh[a], + Op::Rotl | Op::Rotr => fresh[d], + Op::Or | Op::Mul | Op::MulHi => false, + }; + pair_op[d] = if matches!(i.op, Op::Or | Op::Xor) && !masked { Some((i.op, a)) } else { None }; + for r in 0..8 { + if r != d { + if let Some((_, s)) = pair_op[r] { + if s == d { + pair_op[r] = None; + } + } + } + } } Ok(()) } @@ -261,14 +297,15 @@ pub fn check_fresh_sources_v4(p: &Program) -> Result<(), Reject> { return Ok(()); } let mut fresh = [true; 8]; + let mut pair_op: [Option<(Op, usize)>; 8] = [None; 8]; for _ in 0..9 { - let before = fresh; - freshness_pass(p, &mut fresh, false)?; - if fresh == before { + let before = (fresh, pair_op); + freshness_pass(p, &mut fresh, &mut pair_op, false)?; + if (fresh, pair_op) == before { break; } } - freshness_pass(p, &mut fresh, true) + freshness_pass(p, &mut fresh, &mut pair_op, true) } /// Parts (a), (b) and, for class v4 sub-version 2, (a'). @@ -533,7 +570,7 @@ pub fn check_dynamic(p: &Program) -> Result { let v4 = is_class_v4_shape(&p.class); let sites = loads / ITERATIONS; let mut acc = Acc { - sources: if v4 { Some(vec![Vec::with_capacity(ACCEPT_HASHES * ITERATIONS); sites]) } else { None }, + sources: None, indices: None, sat_source: vec![0; sites], and_acc: [u32::MAX; 8], @@ -563,14 +600,7 @@ 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'') (B) on the (c) units' own source values - for (site, values) in acc.sources.take().unwrap().iter_mut().enumerate() { - let (best, best_v) = most_repeated(values); - if best >= MAX_SOURCE_REPEAT_V4 { - return Err(Reject::RepeatedSource { site: site as u8, value: best_v, count: best }); - } - } - // (c'') (A) on 2^20 evaluations per site, the chosen candidate only (after every other test) + // (c''), the ratio on the candidate that passed everything else (the draw's last and dearest test) check_distinct_indices_v4(p)?; } let half = (ACCEPT_HASHES / 2) as u32; diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs index 0aa543d14..9c2ebe05a 100644 --- a/igneum-pow/src/generator.rs +++ b/igneum-pow/src/generator.rs @@ -1266,6 +1266,11 @@ pub fn candidate_from_words_class( && LoadClass { era: None, shadow: None, ..class } == LoadClass { shadow: None, ..V4_CLASS }; let mut fresh = [false; 8]; let mut fresh_value = [true; 8]; + // the shared-operand idiom (AP-F8-1, sub-version 3): after `or d |= s`, a later `xor d ^= s` or `sub d -= s` with + // s unwritten since is `d & ~s`; after `xor d ^= s`, a later `or d |= s` is `d | s`: lossy either way, though + // the second op would count as injecting on its own. `pair_op[d]` holds the (op, s) of the last or/xor on d + // while neither d nor s has been written since. + let mut pair_op: [Option<(Op, usize)>; 8] = [None; 8]; let mut instrs = Vec::with_capacity(INSTR_COUNT); for k in 0..INSTR_COUNT { let mut roll = rng.below(75); @@ -1339,12 +1344,26 @@ pub fn candidate_from_words_class( fresh[src as usize] = false; } fresh[dst as usize] = true; - fresh_value[dst as usize] = match op { - Op::Load | Op::WLoad | Op::Scratch | Op::Hot => fresh_value[src as usize], - Op::Add | Op::Sub | Op::Xor | Op::Mad | Op::Shfl => fresh_value[dst as usize] || fresh_value[src as usize], - Op::Rotl | Op::Rotr => fresh_value[dst as usize], - Op::Or | Op::Mul | Op::MulHi => false, - }; + let (d, a) = (dst as usize, src as usize); + let masked = matches!((pair_op[d], op), (Some((Op::Or, s)), Op::Xor) | (Some((Op::Or, s)), Op::Sub) | (Some((Op::Xor, s)), Op::Or) if s == a); + fresh_value[d] = !masked + && match op { + Op::Load | Op::WLoad | Op::Scratch | Op::Hot => fresh_value[a], + Op::Add | Op::Sub | Op::Xor | Op::Mad | Op::Shfl => fresh_value[d] || fresh_value[a], + Op::Rotl | Op::Rotr => fresh_value[d], + Op::Or | Op::Mul | Op::MulHi => false, + }; + // a write to d sets or clears d's pair; a write to any register clears every pair that names it as operand + pair_op[d] = if matches!(op, Op::Or | Op::Xor) && !masked { Some((op, a)) } else { None }; + for r in 0..8 { + if r != d { + if let Some((_, s)) = pair_op[r] { + if s == d { + pair_op[r] = None; + } + } + } + } instrs.push(Instr { op, dst: dst as u8, src: src as u8, src2: b as u8, imm, imm2, rot, bit: bit as u8, mask, width, win, off }); } // (3) The latency-shadow block (Counter ASIC 3.0 item 8): drawn after the base program from the same stream, so @@ -1865,45 +1884,59 @@ mod tests { crate::accept::check_fresh_sources_v4(p).is_ok() } - /// AP-F8-2: the class v4 draw is total. The last resort turns real (a')-rejected candidates into programs every - /// load of which reads a fresh register, the cap is 256 for the v4 shape and 32 for every other class, and the - /// chain path of a seed whose first candidates are rejected yields a program without a panic. - /// Class v4 sub-version 3 (prepared): the repeated-source pass (c'') on the value-constant class the lineage rule - /// cannot see. Known-failed shapes: F8's p23 (a forced-equal pair feeding `xor` at instruction 0, site 1 reads the - /// zero), p15 (a rotated zero at site 12) and p18 (one word carried load to load, site 14), the chain-shaped seeds - /// and eras of the attack-pass harness, at the attempt sub-version 2 accepted; the known-pass: the devnet epoch-0 - /// program. The timing line per sample size is the draw-cost figure of the ledger. + /// Class v4 sub-version 3, rule (c''): the distinct-index ratio refuses F8's low-entropy band on the chain's own + /// candidates (the first attempt of each strong seed past (a), (b), (c) and (c') fails the 2^20 pass), and the + /// shared-operand rule removes p23's value constant at the draw: the chain's attempt 1 (id d65122675f16a1c7) draws + /// site 7 from r5 and passes; the same program with that source put back to r6 (`or r6 |= r4; xor r6 ^= r4` + /// upstream) is refused by the static rule and, run anyway, by the ratio. #[test] - fn class_v4_repeated_source_pass_rejects_the_value_constant_class() { - let hx = |h: &str| -> Vec { (0..h.len()).step_by(2).map(|i| u8::from_str_radix(&h[i..i + 2], 16).unwrap()).collect() }; - // the exact candidates F8 measured (the attempts sub-version 2's shadow-less acceptance chose: 4, 3 and 2) - let f8 = [ - ("p23", 4u32, "01aa1485fcb5d59223ca40602086e618277decf440188c5a55898f635f7be34f", "00951c99e7ef952fd52611b8de7c07cc705cdec9e129a31a19d696c99909f052"), - ("p15", 3u32, "65d1bc6af696d940f57d7d04469e29dd7c1eb69055619045911121b1dcec8b69", "e6b5324458cfd3c3326baed2aabd49cc361a17bc5efdde9e291daddad21ec5c7"), - ("p18", 2u32, "aa4f71160ecbb87bde40eb5e5731ae84346adcd48b533b26f35c0925d7f40784", "82bb5b72dfbeb137505c844e103906c8b93aacc7f61d3fce1b24d8c0e149c8e2"), - ]; - for (name, attempt, epoch, era) in f8 { - let (e, r) = (hx(epoch), hx(era)); - let label = format!("igneum-epoch/{epoch}"); - let class = LoadClass::era(V4_CLASS, &r, &V3_ALLOWED); - let mut p = candidate_class(&label, &e, attempt, class); + fn class_v4_distinct_ratio_rejects_the_low_entropy_band() { + use crate::accept::{check_dynamic, check_static, Reject}; + 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() }; + let f8 = |k: u32| (w(format!("igneum-attack-f8/program/{k}")), w(format!("igneum-attack-f8/era/{k}"))); + let candidate = |epoch: &[u8], era: &[u8], attempt: u32| { + let label = format!("igneum-epoch/{}", epoch.iter().map(|b| format!("{b:02x}")).collect::()); + let mut p = candidate_class(&label, epoch, attempt, LoadClass::era(V4_CLASS, era, &V3_ALLOWED)); p.generator = GENERATOR_VERSION_V4; - p.era_bytes = Some(r.clone()); - let t0 = std::time::Instant::now(); - let v = crate::accept::check_dynamic(&p).map(|_| ()); - let ms = t0.elapsed().as_secs_f64() * 1e3; - println!("{name}: attempt {} id {:016x}: dynamic check {:?} in {ms:.1} ms", p.attempt, p.program_id(), v.as_ref().err().map(|x| x.to_string())); - assert!(matches!(v, Err(crate::accept::Reject::RepeatedSource { .. }) | Err(crate::accept::Reject::LowEntropySite { .. })), "{name}: the low-entropy class is rejected by (c'')"); + p.era_bytes = Some(era.to_vec()); + p + }; + for k in [15u32, 18, 19, 56] { + let (epoch, era) = f8(k); + let mut seen = None; + for attempt in 0..MAX_ATTEMPTS_V4 { + let p = candidate(&epoch, &era, attempt); + let t0 = std::time::Instant::now(); + match check_static(&p).and_then(|_| check_dynamic(&p).map(|_| ())) { + Err(Reject::LowEntropySite { site, distinct, evaluations, ratio_milli }) => { + println!("p{k} attempt {attempt} id {:016x}: (c'') site {site} read {distinct} distinct over {evaluations}, ratio {}.{:03}, {:.1} s", p.program_id(), ratio_milli / 1000, ratio_milli % 1000, t0.elapsed().as_secs_f64()); + seen = Some(attempt); + break; + } + Err(_) => continue, + Ok(()) => break, + } + } + assert!(seen.is_some(), "p{k}: the chain reaches a candidate only the ratio refuses"); } - let g = hx("edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"); - let p1 = generate_era("igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07", &g, V4_CLASS, &g, &V3_ALLOWED); + let (epoch, era) = f8(23); + let p = candidate(&epoch, &era, 1); + assert_eq!(p.program_id(), 0xd65122675f16a1c7); + let site7 = p.instrs.iter().enumerate().filter(|(_, i)| i.op.is_load()).nth(7).map(|(k, _)| k).unwrap(); + println!("p23 attempt 1: site 7 is instruction {site7}, source r{}", p.instrs[site7].src); + assert_eq!(p.instrs[site7].src, 5, "the shared-operand rule moved site 7 off r6"); + assert!(check_static(&p).is_ok(), "p23 attempt 1 passes the static rule"); let t0 = std::time::Instant::now(); - let v = crate::accept::check_dynamic(&p1).map(|_| ()); - println!("p1: attempt {} id {:016x}: dynamic check {:?} in {:.1} ms (the (c) units with (B), then (A) over 2^20 evaluations per site)", p1.attempt, p1.program_id(), v.as_ref().err().map(|x| x.to_string()), t0.elapsed().as_secs_f64() * 1e3); - assert!(v.is_ok(), "the devnet epoch-0 program passes the dynamic check"); - let t0 = std::time::Instant::now(); - let _ = crate::accept::check_distinct_indices_v4(&p1); - println!("p1: (A) alone in {:.1} ms", t0.elapsed().as_secs_f64() * 1e3); + let v = check_dynamic(&p).map(|_| ()); + println!("p23 attempt 1 dynamic: {:?} in {:.1} s", v.as_ref().err().map(|x| x.to_string()), t0.elapsed().as_secs_f64()); + assert!(v.is_ok(), "p23 attempt 1 passes the dynamic rule"); + let mut q = p.clone(); + q.instrs[site7].src = 6; + assert!(matches!(check_static(&q), Err(Reject::UnfreshLoadSource { .. })), "the value constant's load is refused by the static rule"); + let v = check_dynamic(&q).map(|_| ()); + println!("p23 attempt 1 with site 7 from r6: {:?}", v.as_ref().err().map(|x| x.to_string())); + assert!(matches!(v, Err(Reject::LowEntropySite { .. })), "and by the ratio when run"); } /// Diagnostic (AP-F8-1, p23): the distinct word indices per site on the closed-form words at 2^20 and 2^24 @@ -1926,6 +1959,71 @@ mod tests { } } + /// The threshold measurement for the second sub-version 3 commit: every F8 program (p1 = the devnet epoch-0 seeds, + /// p2 to p64 = the attack-pass harness's label-derived seeds), drawn under this commit's verdicts, each load + /// site's distinct word indices over 2^20 evaluations against the window expectation N - N^2 / 2W, the minimum + /// ratio per seed. Clean seeds set the threshold; the failing seeds of F8's table must sit below it. + #[test] + #[ignore] + fn diag_f8_64_distinct_index_ratios() { + 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() }; + let hx = |h: &str| -> Vec { (0..h.len()).step_by(2).map(|i| u8::from_str_radix(&h[i..i + 2], 16).unwrap()).collect() }; + let n = 1u64 << 20; + for k in 1..=64u32 { + let (epoch, era) = if k == 1 { + let g = hx("edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"); + (g.clone(), g) + } else { + (w(format!("igneum-attack-f8/program/{k}")), w(format!("igneum-attack-f8/era/{k}"))) + }; + let label = format!("igneum-epoch/{}", epoch.iter().map(|b| format!("{b:02x}")).collect::()); + let p = generate_era(&label, &epoch, V4_CLASS, &era, &V3_ALLOWED); + let t0 = std::time::Instant::now(); + let d = crate::accept::distinct_indices_v4(&p, 4096).unwrap(); + let mut ratios = Vec::new(); + let mut site = 0usize; + for i in &p.instrs { + if !i.op.is_load() { continue; } + let k_off = (i.win as u64).min(2); + let wsize = (1u64 << 28) >> k_off; + let expected = n as f64 - (n as f64) * (n as f64) / (2.0 * wsize as f64); + ratios.push((d[site] as f64 / expected, site, i.win)); + site += 1; + } + let (min_ratio, min_site, min_win) = ratios.iter().cloned().fold((9.0, 0, 0), |a, b| if b.0 < a.0 { b } else { a }); + println!("RATIO p{k} attempt {} id {:016x}: min {:.4} at site {} (win {}) ; all {} ; {:.1} s", p.attempt, p.program_id(), min_ratio, min_site, min_win, ratios.iter().map(|r| format!("{:.3}", r.0)).collect::>().join(" "), t0.elapsed().as_secs_f64()); + } + } + + /// The 2^24 reach of the ratio for the weak failing seeds (p34, p4, p8, p10 at 1.22x to 1.50x sit inside the + /// clean spread at 2^20), with p23 and five clean seeds as the scale. + #[test] + #[ignore] + fn diag_f8_weak_seeds_at_2e24() { + 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() }; + let n = 1u64 << 24; + for k in [34u32, 4, 8, 10, 23, 2, 3, 5, 44, 52] { + let (epoch, era) = (w(format!("igneum-attack-f8/program/{k}")), w(format!("igneum-attack-f8/era/{k}"))); + let label = format!("igneum-epoch/{}", epoch.iter().map(|b| format!("{b:02x}")).collect::()); + let p = generate_era(&label, &epoch, V4_CLASS, &era, &V3_ALLOWED); + let t0 = std::time::Instant::now(); + let d = crate::accept::distinct_indices_v4(&p, 65536).unwrap(); + let mut ratios = Vec::new(); + let mut site = 0usize; + for i in &p.instrs { + if !i.op.is_load() { continue; } + let wsize = (1u64 << 28) >> (i.win as u64).min(2); + let expected = n as f64 - (n as f64) * (n as f64) / (2.0 * wsize as f64); + ratios.push(d[site] as f64 / expected); + site += 1; + } + let min = ratios.iter().cloned().fold(9.0f64, f64::min); + println!("RATIO24 p{k} attempt {} id {:016x}: min {:.4} ; all {} ; {:.1} s", p.attempt, p.program_id(), min, ratios.iter().map(|r| format!("{:.3}", r)).collect::>().join(" "), t0.elapsed().as_secs_f64()); + } + } + #[test] fn class_v4_draw_is_total_with_the_last_resort() { assert_eq!(max_attempts_for(&V4_CLASS), MAX_ATTEMPTS_V4);