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 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 14:44:55 +00:00
parent 37dff589f6
commit c3d32437cd
3 changed files with 216 additions and 86 deletions

View file

@ -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 the project lead'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.

View file

@ -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<AcceptReport, Reject> {
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<AcceptReport, Reject> {
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;

View file

@ -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<u8> { (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<u8> { 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::<String>());
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<u8> { seed_words_from_bytes(s.as_bytes()).iter().flat_map(|x| x.to_le_bytes()).collect() };
let hx = |h: &str| -> Vec<u8> { (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::<String>());
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::<Vec<_>>().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<u8> { 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::<String>());
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::<Vec<_>>().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);