diff --git a/docs/analysis/cryptanalysis/report-acceptance-rule-2.md b/docs/analysis/cryptanalysis/report-acceptance-rule-2.md index 5b167a2b4..5a439e16c 100644 --- a/docs/analysis/cryptanalysis/report-acceptance-rule-2.md +++ b/docs/analysis/cryptanalysis/report-acceptance-rule-2.md @@ -117,6 +117,10 @@ Result: PASS with one FINDING of 0.1 percent (a data-dependent rotate by a possi | Home miner (one card, any size, any vendor, any OS), rig, pool user | Nobody gains from grinding headers for locality: the best group a search can find runs 0.09 percent faster on a card and costs 15 hashes to find. Mining stays one header, every nonce. The drawn:0 repeat is 0.1 percent for every miner alike, no tier favoured | Nothing to change in the miner. The repeat class is handed to the rule's owners as a one-line note (a rotate by a register amount as the sole write between two loads from one register) | | A chip that stores the dataset (chip-model-v3 5) | Header choice gives it nothing either; its per-hash read count stays 128 (Q2) and the items per unit stay at 4,095 of 4,096 | The analytic bound of Q3 and the card point stand on their own | +## Queued (lease pool, build-1) + +At 20:32 UK one `lease pool 32 --min 8` job (owner adv-accept-2, label "adv-accept-2 1e8 locality tail + rotclass census", pid file /srv/builds/_adv-adv-accept-2/lease-1e8.pid, log lease-1e8.log) was queued behind the attack-pass F9 class v5 exhaustion leases (0 of 88 pool cores free at 20:33 UK). It runs 33 single-thread jobs at the leased width: 16 shards of 1e8 hashes on each real program (per-shard logs rows-devnet-1e8-s.log, rows-devnet3-1e8-s.log: the 1e-6 tail reading on the same windowed baseline) and the rotate-identity prevalence census over 300 drawn accepted programs (rotclass-300.log). Rows are appended here when they land. Binary sha256 003e540eee23c8621eb1282c2c9a1b4aca981e81337daa20f63e8469d01b1e7e. + ## What a longer pass would add A 1e8-hash sweep per program moves the 1e-5 tail to a 1e-6 reading on the same baseline; a sweep over 1,000 drawn programs counts how often the rotate-identity repeat class appears and at what strength (the two real programs show none; one of eight drawn does); a card point on an RTX 5090 instead of the A6000 reproduces the 0.09 percent at the production read rate. None of these changes the bound. diff --git a/tools/attack/adv-accept-2/src/main.rs b/tools/attack/adv-accept-2/src/main.rs index e900ea2b1..9460686e7 100644 --- a/tools/attack/adv-accept-2/src/main.rs +++ b/tools/attack/adv-accept-2/src/main.rs @@ -461,6 +461,49 @@ fn cmd_export(prog: &str, hashes: u64, units: usize, plant: Plant, best: bool, o log!("export: wrote {} units ({} bytes) to {out}, prog={prog} plant={} best={best}, mean distinct 8 KiB rows {:.2}", units, bytes.len(), plant.name(), r8sum as f64 / units as f64); } +/// Prevalence of the rotate-identity repeat class (the drawn:0 finding): over `count` drawn class v4 programs through +/// the chain draw path (accepted programs), count ordered pairs of load sites (i before j, cyclic over the 64 base +/// instructions plus the shadow block in execution order) that read the SAME source register where every write to that +/// register between them is a `rotr` (identity when its amount register AND 31 is 0: probability 1/32 each). Any other +/// op between them (add, sub, xor, or, mul, mulhi, mad, shfl, load, rotl by 1..31) changes the value except with +/// probability about 2^-32 and ends the chain. Reports pairs, the identity probability per pair, and programs affected. +fn cmd_rotclass(count: u64) { + let (mut affected, mut pairs_total) = (0u64, 0u64); + let mut by_k: std::collections::BTreeMap = std::collections::BTreeMap::new(); + for k in 0..count { + let (p, _) = program_of(&format!("drawn:{k}")); + // one iteration in execution order: base then shadow x reps; a second pass closes the wrap + let order: Vec<&igneum_pow::generator::Instr> = p.instrs.iter().chain((0..p.shadow_reps()).flat_map(|_| p.shadow.iter())).collect(); + let mut found = Vec::new(); + for (ai, a) in order.iter().enumerate() { + if !a.op.is_load() { continue; } + let reg = a.src; + let mut rotrs = 0usize; + let n = order.len(); + for step in 1..n { + let b = order[(ai + step) % n]; + if b.op.is_load() && b.src == reg { + found.push((ai, (ai + step) % n, rotrs)); + break; + } + if b.dst == reg { + if b.op == Op::Rotr { rotrs += 1; continue; } + break; + } + } + } + // a pair with zero rotr writes cannot exist (rule (a) forces a write between two loads from one register) + let real: Vec<_> = found.iter().filter(|(_, _, r)| *r > 0).collect(); + if !real.is_empty() { affected += 1; } + for (a, b, r) in &real { + pairs_total += 1; + *by_k.entry(*r).or_insert(0) += 1; + if real.len() <= 4 { println!(" drawn:{k} id={:#018x} attempt={}: loads at order {a} and {b} read one register with {r} rotr write(s) between: identity probability 32^-{r}", p.program_id(), p.attempt); } + } + } + log!("rotclass: {count} drawn accepted class v4 programs: {affected} affected ({:.2} percent), {pairs_total} load-site pairs whose only intervening writes are rotr; by rotr count: {:?}", 100.0 * affected as f64 / count as f64, by_k); +} + /// Q1b: the predictable prefix. In iteration 0, a load site whose source register has no dataflow path from an earlier /// load's result can be addressed from (I, n) with ALU work alone. Taint: a load taints its dst; add/sub/xor/mad/shfl/ /// or/mul/mulhi/rotr propagate taint from any operand (shfl from the lane group); rotl keeps the dst's taint. Reported @@ -544,6 +587,7 @@ fn main() { "prefix" => cmd_prefix(&args[2..]), "repeats" => cmd_repeats(&prog, hashes), "dump" => cmd_dump(&prog, 64), + "rotclass" => cmd_rotclass(hashes), "export" => cmd_export(&prog, hashes, units_arg, plant, best, &out_arg), "idcheck" => cmd_idcheck(hashes, pairs), _ => usage(),