igneum/tools/attack/f1-shadow/src/main.rs
2026-10-07 10:02:44 +00:00

1498 lines
60 KiB
Rust

//! Attack-pass row F1: shadow block compressibility and shortcut search (docs/plans/cryptanalysis.md 4.2).
//!
//! Target: class v4 (`mx8+sh256x27`): a 256-instruction ALU block run 27 times after instruction 63 of every
//! iteration, with the iteration's `sel` (r0 at the iteration's start) feeding every `add`'s immediate select.
//! Per hash: 8 x 256 x 27 = 55,296 shadow instructions.
//!
//! Method: the 27 repetitions are unrolled symbolically over the 8 lane registers at the iteration's start
//! (symbolic inputs) and `sel` (symbolic per-iteration constants). Every register value is a hash-consed node in
//! normal form:
//! Sum { (node, coeff) } additive chains mod 2^32 (add, sub, mad), constants folded, cancellation
//! Xor { (base, rot, mask) } GF(2)-linear chains (xor, rotl, shfl): rotation and lane-xor-mask atoms, cancellation
//! Or { nodes } or chains, idempotent
//! RotrVar { x, s, k } variable rotates by the same amount register compose (k x (s & 31))
//! Mul { a, b }, Lo, Hi one 64-bit product per operand pair (mul, mulhi, mad share it)
//! A node equal to an existing node costs nothing (identity, cancellation, idempotence, dedupe across the 27
//! repetitions). Every other node is realised either from its normal form (option a) or by its original
//! instruction applied to its predecessor (option b, one instruction), whichever is cheaper, so the compressed
//! count never exceeds the naive count and every local shortcut the rules know is taken.
//!
//! Units: A = ISA instructions (the gate: naive 256 x 27 = 6,912 per iteration, 55,296 per hash);
//! C = chip datapath ops (fixed rotates are wiring, per-iteration constants hoisted; mad and shfl are one op).
//! Counted ops by the 1.83 convention (add 5, rotr 2, shfl 2, the rest 1) are reported as the naive tally.
//!
//! Soundness: every program's normal-form DAG is evaluated concretely on random states and compared with the
//! block run instruction by instruction (the verifier's `step` semantics), and, with the base program emptied, the
//! crate's own `hash_warp` on the same block (the verifier itself). The z3 script under this crate proves window
//! equivalence from the exported JSON.
use igneum_pow::generator::{generate_from_seed_bytes_program_class, Instr, Op, Program, ProgramClass, ITERATIONS, LANES};
use igneum_pow::verify::{hash_warp, splitmix32, DatasetMode, DatasetSource};
use std::collections::HashMap;
use std::fmt::Write as _;
use std::io::Write as _;
const L: usize = LANES;
const REPS: usize = 27;
// ---------------------------------------------------------------------------------------------------------------
// Direct semantics: the block as verify.rs `step` runs it (the ten ALU ops), one rep.
fn run_block(block: &[Instr], r: &mut [[u32; L]; 8], sel: &[u32; L]) {
for ins in block {
let d = ins.dst as usize;
let a = ins.src as usize;
match ins.op {
Op::Add => {
let src = r[a];
for l in 0..L {
let s = (sel[l] >> ins.bit) & 1;
let c = if s != 0 { ins.imm2 } else { ins.imm };
r[d][l] = r[d][l].wrapping_add(src[l]).wrapping_add(c);
}
}
Op::Sub => {
let src = r[a];
for l in 0..L {
r[d][l] = r[d][l].wrapping_sub(src[l]);
}
}
Op::Mul => {
let src = r[a];
for l in 0..L {
r[d][l] = r[d][l].wrapping_mul(src[l]);
}
}
Op::MulHi => {
let src = r[a];
for l in 0..L {
r[d][l] = ((r[d][l] as u64 * src[l] as u64) >> 32) as u32;
}
}
Op::Xor => {
let src = r[a];
for l in 0..L {
r[d][l] ^= src[l];
}
}
Op::Or => {
let src = r[a];
for l in 0..L {
r[d][l] |= src[l];
}
}
Op::Rotl => {
for l in 0..L {
r[d][l] = r[d][l].rotate_left(ins.rot);
}
}
Op::Rotr => {
let src = r[a];
for l in 0..L {
r[d][l] = r[d][l].rotate_right(src[l] & 31);
}
}
Op::Mad => {
let src = r[a];
let src2 = r[ins.src2 as usize];
for l in 0..L {
r[d][l] = src[l].wrapping_mul(src2[l]).wrapping_add(r[d][l]);
}
}
Op::Shfl => {
let src = r[a];
let m = ins.mask as usize;
for l in 0..L {
r[d][l] ^= src[l ^ m];
}
}
_ => panic!("the shadow block holds ALU instructions only"),
}
}
}
fn counted_ops(op: Op) -> usize {
match op {
Op::Add => 5,
Op::Rotr => 2,
Op::Shfl => 2,
_ => 1,
}
}
// ---------------------------------------------------------------------------------------------------------------
// The normal-form DAG.
type Id = u32;
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
enum Node {
Input(u8),
/// Per-iteration lane constant of an `add`: imm2 where bit `bit` of sel is set, else imm.
CSel { bit: u8, imm: u32, imm2: u32 },
/// Additive chain: sum of coeff x node mod 2^32. Sorted by id, coeff != 0, len >= 2 or (len 1, coeff != 1).
Sum { terms: Vec<(Id, u32)> },
/// GF(2) chain: xor over atoms rotl(base, rot) taken from lane ^ mask. Sorted, no duplicate atom,
/// len >= 2 or (len 1 and (rot, mask) != (0, 0)).
Xor { atoms: Vec<(Id, u8, u8)> },
/// Or chain over distinct nodes, sorted, len >= 2.
Or { terms: Vec<Id> },
/// rotr(x, (k x (s & 31)) & 31), k in 1..=31.
RotrVar { x: Id, s: Id, k: u8 },
/// The 64-bit product of two lane values, a <= b.
Mul { a: Id, b: Id },
Lo(Id),
Hi(Id),
Zero,
}
/// Option (b): the instruction that first produced the node, applied to its predecessor (the register's value
/// before it) and the source registers' values.
#[derive(Clone, Copy, Debug)]
struct Deriv {
op: Op,
pred: Id,
src: Id,
src2: Id,
}
#[derive(Default, Clone, Copy, Debug)]
struct Rewrites {
identity: usize,
xor_cancel: usize,
sum_cancel: usize,
or_idem: usize,
rotl_merge: usize,
rotr_merge: usize,
mul_shared: usize,
}
struct Dag {
nodes: Vec<Node>,
index: HashMap<Node, Id>,
is_const: Vec<bool>,
deriv: Vec<Option<Deriv>>,
rw: Rewrites,
/// Deliberately wrong rotl composition, to show the differential test fires.
break_rotl: bool,
/// (result node, created by this instruction) per applied instruction, in order.
trace: Vec<(Id, bool)>,
/// The realisation behind the last `realise`: need flag, option (1 = a, 2 = b), own cost per node.
last_need: Vec<bool>,
last_choice: Vec<u8>,
last_cost: Vec<usize>,
}
impl Dag {
fn new(break_rotl: bool) -> Dag {
Dag { nodes: Vec::new(), index: HashMap::new(), is_const: Vec::new(), deriv: Vec::new(), rw: Rewrites::default(), break_rotl, trace: Vec::new(), last_need: Vec::new(), last_choice: Vec::new(), last_cost: Vec::new() }
}
fn intern(&mut self, n: Node) -> Id {
if let Some(&id) = self.index.get(&n) {
return id;
}
let c = match &n {
Node::Input(_) => false,
Node::CSel { .. } | Node::Zero => true,
Node::Sum { terms } => terms.iter().all(|(t, _)| self.is_const[*t as usize]),
Node::Xor { atoms } => atoms.iter().all(|(t, _, _)| self.is_const[*t as usize]),
Node::Or { terms } => terms.iter().all(|t| self.is_const[*t as usize]),
Node::RotrVar { x, s, .. } => self.is_const[*x as usize] && self.is_const[*s as usize],
Node::Mul { a, b } => self.is_const[*a as usize] && self.is_const[*b as usize],
Node::Lo(p) | Node::Hi(p) => self.is_const[*p as usize],
};
let id = self.nodes.len() as Id;
self.nodes.push(n.clone());
self.index.insert(n, id);
self.is_const.push(c);
self.deriv.push(None);
id
}
fn zero(&mut self) -> Id {
self.intern(Node::Zero)
}
fn sum_terms(&self, id: Id) -> Vec<(Id, u32)> {
match &self.nodes[id as usize] {
Node::Sum { terms } => terms.clone(),
Node::Zero => Vec::new(),
_ => vec![(id, 1)],
}
}
fn xor_atoms(&self, id: Id) -> Vec<(Id, u8, u8)> {
match &self.nodes[id as usize] {
Node::Xor { atoms } => atoms.clone(),
Node::Zero => Vec::new(),
_ => vec![(id, 0, 0)],
}
}
fn or_terms(&self, id: Id) -> Vec<Id> {
match &self.nodes[id as usize] {
Node::Or { terms } => terms.clone(),
Node::Zero => Vec::new(),
_ => vec![id],
}
}
fn make_sum(&mut self, mut terms: Vec<(Id, u32)>) -> Id {
terms.sort_by_key(|t| t.0);
let mut out: Vec<(Id, u32)> = Vec::with_capacity(terms.len());
for (id, c) in terms {
if let Some(last) = out.last_mut() {
if last.0 == id {
last.1 = last.1.wrapping_add(c);
continue;
}
}
out.push((id, c));
}
out.retain(|t| t.1 != 0);
if out.is_empty() {
return self.zero();
}
if out.len() == 1 && out[0].1 == 1 {
return out[0].0;
}
self.intern(Node::Sum { terms: out })
}
fn make_xor(&mut self, mut atoms: Vec<(Id, u8, u8)>) -> Id {
atoms.sort();
let mut out: Vec<(Id, u8, u8)> = Vec::with_capacity(atoms.len());
for a in atoms {
if out.last() == Some(&a) {
out.pop();
} else {
out.push(a);
}
}
if out.is_empty() {
return self.zero();
}
if out.len() == 1 && out[0].1 == 0 && out[0].2 == 0 {
return out[0].0;
}
self.intern(Node::Xor { atoms: out })
}
fn make_or(&mut self, mut terms: Vec<Id>) -> Id {
terms.sort_unstable();
terms.dedup();
let z = self.index.get(&Node::Zero).copied();
if let Some(z) = z {
terms.retain(|&t| t != z);
}
if terms.is_empty() {
return self.zero();
}
if terms.len() == 1 {
return terms[0];
}
self.intern(Node::Or { terms })
}
fn product(&mut self, a: Id, b: Id) -> (Id, bool) {
let (a, b) = if a <= b { (a, b) } else { (b, a) };
let n = Node::Mul { a, b };
let existed = self.index.contains_key(&n);
(self.intern(n), existed)
}
/// Apply one instruction to the symbolic register file.
fn apply(&mut self, ins: &Instr, regs: &mut [Id; 8]) {
let d = ins.dst as usize;
let a = ins.src as usize;
let before = self.nodes.len();
let pred = regs[d];
let src = regs[a];
let src2 = regs[ins.src2 as usize];
let new = match ins.op {
Op::Add => {
let c = self.intern(Node::CSel { bit: ins.bit, imm: ins.imm, imm2: ins.imm2 });
let mut t = self.sum_terms(pred);
let nd = t.iter().filter(|x| !self.is_const[x.0 as usize]).count();
let ts = self.sum_terms(src);
let ns = ts.iter().filter(|x| !self.is_const[x.0 as usize]).count();
t.extend(ts);
t.push((c, 1));
let r = self.make_sum(t);
let nr = self.sum_terms(r).iter().filter(|x| !self.is_const[x.0 as usize]).count();
if nr < nd + ns {
self.rw.sum_cancel += 1;
}
r
}
Op::Sub => {
let mut t = self.sum_terms(pred);
let nd = t.iter().filter(|x| !self.is_const[x.0 as usize]).count();
let ts = self.sum_terms(src);
let ns = ts.iter().filter(|x| !self.is_const[x.0 as usize]).count();
t.extend(ts.into_iter().map(|(i, c)| (i, c.wrapping_neg())));
let r = self.make_sum(t);
let nr = self.sum_terms(r).iter().filter(|x| !self.is_const[x.0 as usize]).count();
if nr < nd + ns {
self.rw.sum_cancel += 1;
}
r
}
Op::Mul => {
let (p, existed) = self.product(pred, src);
if existed {
self.rw.mul_shared += 1;
}
self.intern(Node::Lo(p))
}
Op::MulHi => {
let (p, existed) = self.product(pred, src);
if existed {
self.rw.mul_shared += 1;
}
self.intern(Node::Hi(p))
}
Op::Mad => {
let (p, existed) = self.product(src, src2);
if existed {
self.rw.mul_shared += 1;
}
let lo = self.intern(Node::Lo(p));
let mut t = self.sum_terms(pred);
t.push((lo, 1));
self.make_sum(t)
}
Op::Xor => {
let mut at = self.xor_atoms(pred);
let n0 = at.len();
let s = self.xor_atoms(src);
let n1 = s.len();
at.extend(s);
let r = self.make_xor(at);
if self.xor_atoms(r).len() < n0 + n1 {
self.rw.xor_cancel += 1;
}
r
}
Op::Or => {
let mut t = self.or_terms(pred);
let n0 = t.len();
let s = self.or_terms(src);
let n1 = s.len();
t.extend(s);
let r = self.make_or(t);
if self.or_terms(r).len() < n0 + n1 {
self.rw.or_idem += 1;
}
r
}
Op::Rotl => {
let n = ins.rot as u8 + if self.break_rotl { 1 } else { 0 };
let at = self.xor_atoms(pred);
if at.len() == 1 && at[0].1 != 0 {
self.rw.rotl_merge += 1;
}
let at: Vec<(Id, u8, u8)> = at.into_iter().map(|(b, r, m)| (b, (r + n) % 32, m)).collect();
self.make_xor(at)
}
Op::Rotr => {
if matches!(self.nodes[src as usize], Node::Zero) {
pred
} else if let Node::RotrVar { x, s, k } = self.nodes[pred as usize].clone() {
if s == src {
self.rw.rotr_merge += 1;
let k2 = (k + 1) % 32;
if k2 == 0 {
x
} else {
self.intern(Node::RotrVar { x, s, k: k2 })
}
} else {
self.intern(Node::RotrVar { x: pred, s: src, k: 1 })
}
} else {
self.intern(Node::RotrVar { x: pred, s: src, k: 1 })
}
}
Op::Shfl => {
let mut at = self.xor_atoms(pred);
let n0 = at.len();
let s = self.xor_atoms(src);
let n1 = s.len();
at.extend(s.into_iter().map(|(b, r, m)| (b, r, m ^ ins.mask)));
let r = self.make_xor(at);
if self.xor_atoms(r).len() < n0 + n1 {
self.rw.xor_cancel += 1;
}
r
}
_ => panic!("the shadow block holds ALU instructions only"),
};
if (new as usize) < before {
// the result is a node that already existed: this instruction costs nothing
self.rw.identity += 1;
} else if self.deriv[new as usize].is_none() {
self.deriv[new as usize] = Some(Deriv { op: ins.op, pred, src, src2 });
}
self.trace.push((new, (new as usize) >= before));
regs[d] = new;
}
/// Children of the normal form (option a) and its own cost, per unit. Interns the single-atom nodes of a
/// multi-atom Xor (a rotated or permuted atom is materialised once and shared).
fn option_a(&mut self, id: Id, chip: bool) -> (usize, Vec<Id>) {
if self.is_const[id as usize] {
return (0, Vec::new());
}
match self.nodes[id as usize].clone() {
Node::Input(_) | Node::CSel { .. } | Node::Zero => (0, Vec::new()),
Node::Sum { terms } => {
let n = terms.iter().filter(|t| !self.is_const[t.0 as usize]).count();
let c = terms.len() - n;
let mut cost = n.saturating_sub(1) + if c > 0 && n <= 1 { 1 } else { 0 };
for (_, k) in &terms {
let k = *k;
if !(k.is_power_of_two() || k.wrapping_neg().is_power_of_two()) {
cost += 1;
}
}
(cost, terms.iter().map(|t| t.0).collect())
}
Node::Xor { atoms } => {
if atoms.len() == 1 {
let (b, r, m) = atoms[0];
let cost = if m != 0 { 1 + if r != 0 && !chip { 1 } else { 0 } } else if chip { 0 } else { 1 };
return (cost, vec![b]);
}
let mut ch = Vec::with_capacity(atoms.len());
for (b, r, m) in atoms.iter().copied() {
if r == 0 && m == 0 {
ch.push(b);
} else {
ch.push(self.intern(Node::Xor { atoms: vec![(b, r, m)] }));
}
}
(atoms.len() - 1, ch)
}
Node::Or { terms } => (terms.len() - 1, terms),
Node::RotrVar { x, s, .. } => (1, vec![x, s]),
Node::Mul { a, b } => (1, vec![a, b]),
Node::Lo(p) | Node::Hi(p) => (0, vec![p]),
}
}
fn option_b(&self, id: Id, chip: bool) -> Option<(usize, Vec<Id>)> {
let d = self.deriv[id as usize]?;
let cost = if chip && d.op == Op::Rotl { 0 } else { 1 };
let mut ch = vec![d.pred, d.src];
if d.op == Op::Mad {
ch.push(d.src2);
}
Some((cost, ch))
}
/// The realised cost of the outputs: a needed set with one option per node, iterated to a fixpoint from the
/// greedy choice. Returns (cost, needed count, derived nodes not needed, option-a count).
fn realise(&mut self, outputs: &[Id], chip: bool) -> (usize, usize, usize, usize) {
let n = self.nodes.len();
// pre-intern the atom nodes option (a) may add, so ids are stable during the passes
for id in 0..n as Id {
let _ = self.option_a(id, chip);
}
let n = self.nodes.len();
let mut needed = vec![false; n];
let mut choice = vec![0u8; n]; // 0 = undecided, 1 = a, 2 = b
let mut best: Option<(usize, usize, usize, usize)> = None;
let mut prev_needed: Option<Vec<bool>> = None;
for _pass in 0..8 {
// reverse topological: ids increase with creation, every child has a lower id
let mut need = vec![false; n];
for &o in outputs {
need[o as usize] = true;
}
let mut cost = 0usize;
let mut na = 0usize;
let mut own = vec![0usize; n];
let mut done = vec![false; n];
let mut stack: Vec<Id> = Vec::new();
for id in (0..n as Id).rev() {
if !need[id as usize] || done[id as usize] {
continue;
}
stack.push(id);
// a child with a higher id (an atom node interned after its consumer) is processed as soon as it
// becomes needed, so its cost and its subtree are never dropped by the descending sweep
while let Some(id) = stack.pop() {
if done[id as usize] {
continue;
}
done[id as usize] = true;
let (ca, cha) = self.option_a(id, chip);
let fa = ca + cha.iter().map(|&c| self.estimate(c, chip, &need, &needed, 3)).sum::<usize>();
let (c, ch) = match self.option_b(id, chip) {
Some((cb, chb)) => {
let fb = cb + chb.iter().map(|&c| self.estimate(c, chip, &need, &needed, 3)).sum::<usize>();
if fa <= fb {
choice[id as usize] = 1;
(ca, cha)
} else {
choice[id as usize] = 2;
(cb, chb)
}
}
None => {
choice[id as usize] = 1;
(ca, cha)
}
};
if choice[id as usize] == 1 {
na += 1;
}
own[id as usize] = c;
cost += c;
for c in ch {
need[c as usize] = true;
if c > id && !done[c as usize] {
stack.push(c);
}
}
}
}
let nn = need.iter().filter(|&&b| b).count();
let dead = (0..n).filter(|&i| self.deriv[i].is_some() && !need[i]).count();
if best.map_or(true, |b| cost < b.0) {
best = Some((cost, nn, dead, na));
self.last_need = need.clone();
self.last_choice = choice.clone();
self.last_cost = own;
}
if prev_needed.as_ref() == Some(&need) {
break;
}
prev_needed = Some(need.clone());
needed = need;
}
// the all-(b) baseline: every derived node by its own instruction, identities free
let mut need = vec![false; n];
for &o in outputs {
need[o as usize] = true;
}
let mut cost = 0usize;
let mut own = vec![0usize; n];
let mut choice = vec![0u8; n];
for id in (0..n as Id).rev() {
if !need[id as usize] {
continue;
}
let (c, ch) = match self.option_b(id, chip) {
Some(x) => {
choice[id as usize] = 2;
x
}
None => {
choice[id as usize] = 1;
self.option_a(id, chip)
}
};
own[id as usize] = c;
cost += c;
for c in ch {
need[c as usize] = true;
}
}
let nn = need.iter().filter(|&&b| b).count();
let dead = (0..n).filter(|&i| self.deriv[i].is_some() && !need[i]).count();
if best.map_or(true, |b| cost < b.0) {
best = Some((cost, nn, dead, 0));
self.last_need = need;
self.last_choice = choice;
self.last_cost = own;
}
best.unwrap()
}
/// What making `id` needed would add, if it is not needed yet: its cheaper option plus its own fresh children,
/// to `depth` levels.
fn estimate(&mut self, id: Id, chip: bool, need: &[bool], needed: &[bool], depth: u32) -> usize {
let i = id as usize;
if need[i] || needed[i] || self.is_const[i] || matches!(self.nodes[i], Node::Input(_)) {
return 0;
}
if depth == 0 {
return 1;
}
let (ca, cha) = self.option_a(id, chip);
let ea = ca + cha.iter().map(|&c| self.estimate(c, chip, need, needed, depth - 1)).sum::<usize>();
match self.option_b(id, chip) {
Some((cb, chb)) => {
let eb = cb + chb.iter().map(|&c| self.estimate(c, chip, need, needed, depth - 1)).sum::<usize>();
ea.min(eb)
}
None => ea,
}
}
}
// ---------------------------------------------------------------------------------------------------------------
// Concrete evaluation of the normal forms.
struct Eval<'a> {
dag: &'a Dag,
vals: Vec<Option<[u32; L]>>,
inputs: [[u32; L]; 8],
sel: [u32; L],
}
impl<'a> Eval<'a> {
fn get(&mut self, id: Id) -> [u32; L] {
if let Some(v) = self.vals[id as usize] {
return v;
}
let v = match self.dag.nodes[id as usize].clone() {
Node::Input(r) => self.inputs[r as usize],
Node::CSel { bit, imm, imm2 } => {
let mut o = [0u32; L];
for l in 0..L {
o[l] = if (self.sel[l] >> bit) & 1 != 0 { imm2 } else { imm };
}
o
}
Node::Zero => [0u32; L],
Node::Sum { terms } => {
let mut o = [0u32; L];
for (t, c) in terms {
let v = self.get(t);
for l in 0..L {
o[l] = o[l].wrapping_add(v[l].wrapping_mul(c));
}
}
o
}
Node::Xor { atoms } => {
let mut o = [0u32; L];
for (b, r, m) in atoms {
let v = self.get(b);
for l in 0..L {
o[l] ^= v[l ^ m as usize].rotate_left(r as u32);
}
}
o
}
Node::Or { terms } => {
let mut o = [0u32; L];
for t in terms {
let v = self.get(t);
for l in 0..L {
o[l] |= v[l];
}
}
o
}
Node::RotrVar { x, s, k } => {
let xv = self.get(x);
let sv = self.get(s);
let mut o = [0u32; L];
for l in 0..L {
o[l] = xv[l].rotate_right(((k as u32) * (sv[l] & 31)) & 31);
}
o
}
Node::Mul { .. } => panic!("a product is read through Lo or Hi"),
Node::Lo(p) => {
let (a, b) = match self.dag.nodes[p as usize] {
Node::Mul { a, b } => (a, b),
_ => unreachable!(),
};
let av = self.get(a);
let bv = self.get(b);
let mut o = [0u32; L];
for l in 0..L {
o[l] = av[l].wrapping_mul(bv[l]);
}
o
}
Node::Hi(p) => {
let (a, b) = match self.dag.nodes[p as usize] {
Node::Mul { a, b } => (a, b),
_ => unreachable!(),
};
let av = self.get(a);
let bv = self.get(b);
let mut o = [0u32; L];
for l in 0..L {
o[l] = ((av[l] as u64 * bv[l] as u64) >> 32) as u32;
}
o
}
};
self.vals[id as usize] = Some(v);
v
}
}
// ---------------------------------------------------------------------------------------------------------------
// One program's analysis.
struct Report {
idx: usize,
seed: String,
attempt: u32,
naive_a: usize,
cost_a: usize,
naive_c: usize,
cost_c: usize,
naive_ops: usize,
nodes: usize,
needed: usize,
unneeded_derived: usize,
option_a: usize,
consts: usize,
rw: Rewrites,
diff_ok: bool,
verify_ok: Option<bool>,
}
impl Report {
fn save_a(&self) -> f64 {
100.0 * (1.0 - self.cost_a as f64 / self.naive_a as f64)
}
fn save_c(&self) -> f64 {
100.0 * (1.0 - self.cost_c as f64 / self.naive_c as f64)
}
fn csv(&self) -> String {
let r = self.rw;
format!(
"{},{},{},{},{},{:.4},{},{},{:.4},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
self.idx,
self.seed,
self.attempt,
self.naive_a,
self.cost_a,
self.save_a(),
self.naive_c,
self.cost_c,
self.save_c(),
self.naive_ops,
self.nodes,
self.needed,
self.unneeded_derived,
self.option_a,
self.consts,
r.identity,
r.xor_cancel,
r.sum_cancel,
r.or_idem,
r.rotl_merge,
r.rotr_merge,
r.mul_shared,
if self.diff_ok { "ok" } else { "FAIL" },
match self.verify_ok {
Some(true) => "ok",
Some(false) => "FAIL",
None => "skip",
}
)
}
}
const CSV_HEADER: &str = "idx,seed,attempt,naive_instrs,cost_instrs,save_instrs_pct,naive_chip,cost_chip,save_chip_pct,naive_counted_ops,nodes,needed,unneeded_derived,option_a,consts,rw_identity,rw_xor_cancel,rw_sum_cancel,rw_or_idem,rw_rotl_merge,rw_rotr_merge,rw_mul_shared,difftest,verify";
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
z ^ (z >> 31)
}
fn u32(&mut self) -> u32 {
self.next() as u32
}
}
struct Opts {
reps: usize,
fold_regs: usize,
break_rotl: bool,
diff_states: usize,
verify: bool,
}
/// Build the unrolled DAG of `reps` passes of `block` and realise it.
fn analyse_block(block: &[Instr], opts: &Opts) -> (Dag, [Id; 8], Rewrites) {
let mut dag = Dag::new(opts.break_rotl);
let mut regs = [0 as Id; 8];
for r in 0..8 {
regs[r] = dag.intern(Node::Input(r as u8));
}
for _ in 0..opts.reps {
for ins in block {
dag.apply(ins, &mut regs);
}
}
let rw = dag.rw;
(dag, regs, rw)
}
fn init_regs(seed: &[u32; 8], base_nonce: u32) -> [[u32; L]; 8] {
let mut r = [[0u32; L]; 8];
for lane in 0..L {
let nonce = base_nonce.wrapping_add(lane as u32);
for i in 0..8 {
let mut x = nonce ^ seed[i];
x = x.wrapping_add(0x9e3779b9u32.wrapping_mul(i as u32 + 1));
x = splitmix32(x);
r[i][lane] = x ^ seed[(i + 1) & 7];
}
}
r
}
fn analyse(idx: usize, seed: &str, program: &Program, block: &[Instr], opts: &Opts, ds: Option<&DatasetSource>) -> Report {
let (mut dag, regs, rw) = analyse_block(block, opts);
let outputs: Vec<Id> = regs[..opts.fold_regs].to_vec();
let nodes = dag.nodes.len();
let consts = dag.is_const.iter().filter(|&&c| c).count();
let (cost_a, needed, unneeded, option_a) = dag.realise(&outputs, false);
let (cost_c, _, _, _) = dag.realise(&outputs, true);
let naive_a = block.len() * opts.reps;
let naive_c = block.iter().filter(|i| i.op != Op::Rotl).count() * opts.reps;
let naive_ops = block.iter().map(|i| counted_ops(i.op)).sum::<usize>() * opts.reps;
// Differential test: the normal forms against the instruction-by-instruction run on random states.
let mut rng = Rng(0x5eed_0000_0000_0000 ^ idx as u64);
let mut diff_ok = true;
for _ in 0..opts.diff_states {
let mut inputs = [[0u32; L]; 8];
let mut sel = [0u32; L];
for r in 0..8 {
for l in 0..L {
inputs[r][l] = rng.u32();
}
}
for l in 0..L {
sel[l] = rng.u32();
}
let mut direct = inputs;
for _ in 0..opts.reps {
run_block(block, &mut direct, &sel);
}
let mut ev = Eval { dag: &dag, vals: vec![None; dag.nodes.len()], inputs, sel };
for r in 0..opts.fold_regs {
if ev.get(regs[r]) != direct[r] {
diff_ok = false;
}
}
}
// The verifier itself: the program with its base instructions removed runs 8 iterations of the shadow alone.
let verify_ok = if opts.verify && opts.reps == REPS && opts.fold_regs == 8 && !opts.break_rotl {
let ds = ds.expect("dataset source");
let mut p = program.clone();
p.instrs = Vec::new();
p.shadow = block.to_vec();
let base_nonce = 0x1000 * idx as u32;
let want = hash_warp(&p, base_nonce, ds);
let mut r = init_regs(&p.seed, base_nonce);
for _ in 0..ITERATIONS {
let sel = r[0];
let mut ev = Eval { dag: &dag, vals: vec![None; dag.nodes.len()], inputs: r, sel };
let mut next = [[0u32; L]; 8];
for k in 0..8 {
next[k] = ev.get(regs[k]);
}
r = next;
}
let mut ok = true;
for lane in 0..L {
let lo = r[0][lane] ^ r[1][lane].rotate_left(7) ^ r[2][lane].rotate_left(14) ^ r[3][lane].rotate_left(21);
let hi = r[4][lane] ^ r[5][lane].rotate_left(9) ^ r[6][lane].rotate_left(18) ^ r[7][lane].rotate_left(27);
if ((hi as u64) << 32) | lo as u64 != want[lane] {
ok = false;
}
}
Some(ok)
} else {
None
};
Report {
idx,
seed: seed.to_string(),
attempt: program.attempt,
naive_a,
cost_a,
naive_c,
cost_c,
naive_ops,
nodes,
needed,
unneeded_derived: unneeded,
option_a,
consts,
rw,
diff_ok,
verify_ok,
}
}
fn program_for(seed: &str) -> Program {
generate_from_seed_bytes_program_class(seed, seed.as_bytes(), ProgramClass::V4, None)
}
// ---------------------------------------------------------------------------------------------------------------
// Plants (the known-fail firings).
/// A deliberately compressible block: the seed's real block with its first 65 slots overwritten by
/// 10 x (xor d,a; xor d,a), 5 x (rotl d; rotl d; rotl d), 5 x (add d,a; sub d,a), 5 x (or d,a; or d,a),
/// 5 x (shfl d,a,m; shfl d,a,m). 65 instructions whose minimum is 15: 50 of 256 saved per pass (19.5 percent).
fn plant_compressible(block: &[Instr]) -> Vec<Instr> {
let mut b = block.to_vec();
let mut rng = Rng(0x9a17);
let mut i = 0usize;
let pair = |b: &mut Vec<Instr>, i: &mut usize, op: Op, n: usize, rng: &mut Rng| {
let d = (rng.u32() % 8) as u8;
let mut a = (rng.u32() % 8) as u8;
if a == d {
a = (a + 1) % 8;
}
let mask = 1u8 << (rng.u32() % 5);
for k in 0..n {
let t = &mut b[*i];
t.op = op;
t.dst = d;
t.src = a;
t.mask = mask;
t.rot = 3 + (k as u32 * 5) % 29;
if op == Op::Add && k == 1 {
t.op = Op::Sub;
}
*i += 1;
}
};
for _ in 0..10 {
pair(&mut b, &mut i, Op::Xor, 2, &mut rng);
}
for _ in 0..5 {
pair(&mut b, &mut i, Op::Rotl, 3, &mut rng);
}
for _ in 0..5 {
pair(&mut b, &mut i, Op::Add, 2, &mut rng);
}
for _ in 0..5 {
pair(&mut b, &mut i, Op::Or, 2, &mut rng);
}
for _ in 0..5 {
pair(&mut b, &mut i, Op::Shfl, 2, &mut rng);
}
assert_eq!(i, 65);
b
}
/// The same patterns with the source register rotated between the two halves (`rotl a`), so no pair cancels,
/// merges or restores: the compressor must report no more than the real block's own saving.
fn plant_modified(block: &[Instr]) -> Vec<Instr> {
let b = plant_compressible(block);
let planted: Vec<Instr> = b[..65].to_vec();
let mut out = Vec::new();
let mut k = 0;
while k < planted.len() {
let group = if planted[k].op == Op::Rotl { 3 } else { 2 };
for j in 0..group {
out.push(planted[k + j]);
if j + 1 < group {
let mut rd = planted[k + j];
rd.op = Op::Rotl;
rd.dst = planted[k + j].src;
rd.rot = 11;
if planted[k + j].op == Op::Rotl {
// a rotl pair has no source: rotate a value the pair's register is then xored with
rd.dst = (planted[k + j].dst + 3) % 8;
out.push(rd);
let mut x = planted[k + j];
x.op = Op::Xor;
x.src = rd.dst;
out.push(x);
} else {
out.push(rd);
}
}
}
k += group;
}
out.extend_from_slice(&b[65..]);
out.truncate(256);
out
}
/// The real block with its last instruction replaced by `rotl r7` (never read afterwards): under a fold over
/// registers 0 to 6 that write is dead.
fn plant_dead_tail(block: &[Instr]) -> Vec<Instr> {
let mut b = block.to_vec();
let last = b.len() - 1;
b[last].op = Op::Rotl;
b[last].dst = 7;
b[last].src = 0;
b[last].rot = 5;
b
}
/// The same patterns with a read of `d` between the two halves (`xor e, d`): the second half of an xor, or or
/// shfl pair then RESTORES a value the chip still holds (a zero-op copy), so a saving here is real; rotl and
/// add/sub pairs cannot merge across the read. Reported for information.
fn plant_interrupted(block: &[Instr]) -> Vec<Instr> {
let mut b = plant_compressible(block);
// insert reads: overwrite slots 65.. with `xor e, d` for each planted d? The pairs are consecutive, so a read
// must sit between them: rebuild the first 65 slots as triples (op; read; op) over slots 0..97.
let planted: Vec<Instr> = b[..65].to_vec();
let mut out = Vec::new();
let mut k = 0;
while k < planted.len() {
let group = if planted[k].op == Op::Rotl { 3 } else { 2 };
for j in 0..group {
out.push(planted[k + j]);
if j + 1 < group {
let mut rd = planted[k + j];
rd.op = Op::Xor;
rd.src = planted[k + j].dst;
rd.dst = (planted[k + j].dst + 1) % 8;
out.push(rd);
}
}
k += group;
}
// out has 65 + 35 = 100 instructions; the rest of the block follows, truncated to 256
out.extend_from_slice(&b[65..]);
out.truncate(256);
b = out;
b
}
// ---------------------------------------------------------------------------------------------------------------
// JSON export for the z3 window proofs.
fn instr_json(ins: &Instr) -> String {
format!(
"{{\"op\":\"{}\",\"dst\":{},\"src\":{},\"src2\":{},\"imm\":{},\"imm2\":{},\"rot\":{},\"bit\":{},\"mask\":{}}}",
ins.op.name(),
ins.dst,
ins.src,
ins.src2,
ins.imm,
ins.imm2,
ins.rot,
ins.bit,
ins.mask
)
}
fn dag_json(dag: &Dag, outputs: &[Id]) -> String {
// nodes reachable from the outputs by normal form
let mut reach = vec![false; dag.nodes.len()];
let mut stack: Vec<Id> = outputs.to_vec();
while let Some(id) = stack.pop() {
if reach[id as usize] {
continue;
}
reach[id as usize] = true;
match &dag.nodes[id as usize] {
Node::Input(_) | Node::CSel { .. } | Node::Zero => {}
Node::Sum { terms } => stack.extend(terms.iter().map(|t| t.0)),
Node::Xor { atoms } => stack.extend(atoms.iter().map(|t| t.0)),
Node::Or { terms } => stack.extend(terms.iter()),
Node::RotrVar { x, s, .. } => {
stack.push(*x);
stack.push(*s);
}
Node::Mul { a, b } => {
stack.push(*a);
stack.push(*b);
}
Node::Lo(p) | Node::Hi(p) => stack.push(*p),
}
}
let mut s = String::from("[");
let mut first = true;
for (i, n) in dag.nodes.iter().enumerate() {
if !reach[i] {
continue;
}
if !first {
s.push(',');
}
first = false;
let body = match n {
Node::Input(r) => format!("\"k\":\"in\",\"r\":{r}"),
Node::CSel { bit, imm, imm2 } => format!("\"k\":\"csel\",\"bit\":{bit},\"imm\":{imm},\"imm2\":{imm2}"),
Node::Zero => "\"k\":\"zero\"".to_string(),
Node::Sum { terms } => format!("\"k\":\"sum\",\"t\":[{}]", terms.iter().map(|(i, c)| format!("[{i},{c}]")).collect::<Vec<_>>().join(",")),
Node::Xor { atoms } => format!("\"k\":\"xor\",\"t\":[{}]", atoms.iter().map(|(i, r, m)| format!("[{i},{r},{m}]")).collect::<Vec<_>>().join(",")),
Node::Or { terms } => format!("\"k\":\"or\",\"t\":[{}]", terms.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(",")),
Node::RotrVar { x, s, k } => format!("\"k\":\"rotr\",\"x\":{x},\"s\":{s},\"n\":{k}"),
Node::Mul { a, b } => format!("\"k\":\"mul\",\"a\":{a},\"b\":{b}"),
Node::Lo(p) => format!("\"k\":\"lo\",\"p\":{p}"),
Node::Hi(p) => format!("\"k\":\"hi\",\"p\":{p}"),
};
let _ = write!(s, "{{\"id\":{i},{body}}}");
}
s.push(']');
s
}
fn export_windows(seed: &str, block: &[Instr], w: usize, stride: usize, reps: usize, path: &str) {
let mut f = std::fs::File::create(path).expect("create json");
let unrolled: Vec<Instr> = (0..reps).flat_map(|_| block.iter().copied()).collect();
let mut out = String::from("[");
let mut start = 0;
let mut first = true;
while start < unrolled.len() {
let end = (start + w).min(unrolled.len());
let win = &unrolled[start..end];
let opts = Opts { reps: 1, fold_regs: 8, break_rotl: false, diff_states: 0, verify: false };
let (dag, regs, _) = analyse_block(win, &opts);
if !first {
out.push(',');
}
first = false;
let _ = write!(
out,
"{{\"seed\":\"{}\",\"start\":{},\"instrs\":[{}],\"nodes\":{},\"outputs\":[{}]}}",
seed,
start,
win.iter().map(instr_json).collect::<Vec<_>>().join(","),
dag_json(&dag, &regs),
regs.iter().map(|r| r.to_string()).collect::<Vec<_>>().join(",")
);
if end == unrolled.len() {
break;
}
start += stride;
}
out.push(']');
f.write_all(out.as_bytes()).expect("write json");
}
// ---------------------------------------------------------------------------------------------------------------
fn arg(args: &[String], name: &str) -> Option<String> {
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned())
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("help");
match cmd {
"census" => census(&args),
"one" => one(&args),
"plant" => plant(&args),
"windows" => windows(&args),
"explain" => explain(&args),
"emit-c" => emit_c(&args),
_ => {
eprintln!("attack-f1 census --count N --threads T --out DIR [--prefix attack-f1/] [--diff-states K] [--no-verify]");
eprintln!("attack-f1 one --seed S [--reps R] [--fold-regs K] [--break-rotl] [--plant compressible|interrupted]");
eprintln!("attack-f1 plant --seed S the known-pass and known-fail firings");
eprintln!("attack-f1 windows --seed S --out F.json [--window 16] [--stride 8] [--reps 1]");
eprintln!("attack-f1 explain --seed S [--reps 1] per-instruction realisation of the block");
eprintln!("attack-f1 emit-c --seed S --out F.c one pass as scalar C (compiler proxy)");
std::process::exit(2);
}
}
}
fn one(args: &[String]) {
let seed = arg(args, "--seed").unwrap_or_else(|| "attack-f1/0".into());
let reps: usize = arg(args, "--reps").map(|s| s.parse().unwrap()).unwrap_or(REPS);
let fold_regs: usize = arg(args, "--fold-regs").map(|s| s.parse().unwrap()).unwrap_or(8);
let break_rotl = args.iter().any(|a| a == "--break-rotl");
let p = program_for(&seed);
let block: Vec<Instr> = match arg(args, "--plant").as_deref() {
Some("compressible") => plant_compressible(&p.shadow),
Some("interrupted") => plant_interrupted(&p.shadow),
Some("modified") => plant_modified(&p.shadow),
Some("dead-tail") => plant_dead_tail(&p.shadow),
_ => p.shadow.clone(),
};
let opts = Opts { reps, fold_regs, break_rotl, diff_states: 64, verify: true };
let ds = DatasetSource::new("attack-f1", DatasetMode::ClosedForm, 20);
let r = analyse(0, &seed, &p, &block, &opts, Some(&ds));
println!("{CSV_HEADER}");
println!("{}", r.csv());
println!(
"seed {} attempt {} block {} reps {} fold {}: instructions {} -> {} ({:.3}% saved); chip ops {} -> {} ({:.3}% saved); counted ops naive {}; difftest {}; verify {}",
seed,
r.attempt,
block.len(),
reps,
fold_regs,
r.naive_a,
r.cost_a,
r.save_a(),
r.naive_c,
r.cost_c,
r.save_c(),
r.naive_ops,
if r.diff_ok { "ok" } else { "MISMATCH" },
match r.verify_ok {
Some(true) => "ok",
Some(false) => "MISMATCH",
None => "skipped",
}
);
if !r.diff_ok || r.verify_ok == Some(false) {
std::process::exit(1);
}
}
fn plant(args: &[String]) {
let seed = arg(args, "--seed").unwrap_or_else(|| "attack-f1/0".into());
let p = program_for(&seed);
let ds = DatasetSource::new("attack-f1", DatasetMode::ClosedForm, 20);
let base = Opts { reps: REPS, fold_regs: 8, break_rotl: false, diff_states: 64, verify: true };
println!("{CSV_HEADER}");
let real = analyse(0, &seed, &p, &p.shadow, &base, Some(&ds));
println!("real,{}", real.csv());
let planted = plant_compressible(&p.shadow);
let pl = analyse(0, &seed, &p, &planted, &base, Some(&ds));
println!("planted-compressible,{}", pl.csv());
let inter = plant_interrupted(&p.shadow);
let it = analyse(0, &seed, &p, &inter, &base, Some(&ds));
println!("planted-interrupted,{}", it.csv());
let modified = plant_modified(&p.shadow);
let md = analyse(0, &seed, &p, &modified, &base, Some(&ds));
println!("planted-modified,{}", md.csv());
let broken = Opts { reps: REPS, fold_regs: 8, break_rotl: true, diff_states: 8, verify: false };
let br = analyse(0, &seed, &p, &p.shadow, &broken, None);
println!("broken-rotl-rule,{}", br.csv());
let tail = plant_dead_tail(&p.shadow);
let dead = Opts { reps: 1, fold_regs: 7, break_rotl: false, diff_states: 8, verify: false };
let dd = analyse(0, &seed, &p, &tail, &dead, None);
println!("dead-tail-fold-7-regs-1-rep,{}", dd.csv());
let alive = Opts { reps: 1, fold_regs: 8, break_rotl: false, diff_states: 8, verify: false };
let al = analyse(0, &seed, &p, &tail, &alive, None);
println!("dead-tail-fold-8-regs-1-rep,{}", al.csv());
println!();
println!("FIRINGS");
println!("known-pass (real block): saved {:.3}% instructions, difftest {}, verify {}", real.save_a(), real.diff_ok, real.verify_ok == Some(true));
println!("known-fail (planted 50/256): saved {:.3}% instructions (expected about {:.1}% + the real block's own), difftest {}, verify {}", pl.save_a(), 100.0 * 50.0 / 256.0, pl.diff_ok, pl.verify_ok == Some(true));
println!("must-not-fire (planted, source rotated between): saved {:.3}% instructions (expected about the real block's own), difftest {}, verify {}", md.save_a(), md.diff_ok, md.verify_ok == Some(true));
println!("information (planted, read between): saved {:.3}% instructions (the restore shortcut: xor, or and shfl pairs restore a held value), difftest {}", it.save_a(), it.diff_ok);
println!("soundness firing (rotl rule broken on purpose): difftest {} (MISMATCH expected)", if br.diff_ok { "ok (NOT FIRED)" } else { "MISMATCH" });
println!("dead-code pass: last instruction rotl r7, 1 rep: fold over 7 registers costs {} and leaves {} derived nodes unneeded; over 8 registers {} and {} (one more needed, one fewer unneeded expected)", dd.cost_a, dd.unneeded_derived, al.cost_a, al.unneeded_derived);
let ok = real.diff_ok
&& real.verify_ok == Some(true)
&& pl.save_a() >= 15.0
&& pl.diff_ok
&& pl.verify_ok == Some(true)
&& md.save_a() < 3.0
&& md.diff_ok
&& !br.diff_ok
&& dd.cost_a + 1 == al.cost_a
&& dd.unneeded_derived == al.unneeded_derived + 1;
println!("plant verdict: {}", if ok { "ALL FIRINGS AS EXPECTED" } else { "A FIRING DID NOT BEHAVE" });
if !ok {
std::process::exit(1);
}
}
fn windows(args: &[String]) {
let seed = arg(args, "--seed").unwrap_or_else(|| "attack-f1/0".into());
let out = arg(args, "--out").expect("--out");
let w: usize = arg(args, "--window").map(|s| s.parse().unwrap()).unwrap_or(16);
let stride: usize = arg(args, "--stride").map(|s| s.parse().unwrap()).unwrap_or(8);
let reps: usize = arg(args, "--reps").map(|s| s.parse().unwrap()).unwrap_or(1);
let p = program_for(&seed);
export_windows(&seed, &p.shadow, w, stride, reps, &out);
eprintln!("wrote {out}");
}
fn node_text(dag: &Dag, id: Id) -> String {
match &dag.nodes[id as usize] {
Node::Input(r) => format!("in{r}"),
Node::CSel { bit, .. } => format!("csel(b{bit})"),
Node::Zero => "0".into(),
Node::Sum { terms } => format!("sum{{{}}}", terms.iter().map(|(i, c)| if *c == 1 { format!("#{i}") } else if *c == u32::MAX { format!("-#{i}") } else { format!("{c}*#{i}") }).collect::<Vec<_>>().join("+")),
Node::Xor { atoms } => format!("xor{{{}}}", atoms.iter().map(|(i, r, m)| format!("#{i}{}{}", if *r != 0 { format!("<<{r}") } else { String::new() }, if *m != 0 { format!("^l{m}") } else { String::new() })).collect::<Vec<_>>().join(" ^ ")),
Node::Or { terms } => format!("or{{{}}}", terms.iter().map(|i| format!("#{i}")).collect::<Vec<_>>().join("|")),
Node::RotrVar { x, s, k } => format!("rotr(#{x}, {k}*#{s})"),
Node::Mul { a, b } => format!("mul64(#{a},#{b})"),
Node::Lo(p) => format!("lo(#{p})"),
Node::Hi(p) => format!("hi(#{p})"),
}
}
fn explain(args: &[String]) {
let seed = arg(args, "--seed").unwrap_or_else(|| "attack-f1/0".into());
let reps: usize = arg(args, "--reps").map(|s| s.parse().unwrap()).unwrap_or(1);
let p = program_for(&seed);
let block = p.shadow.clone();
let opts = Opts { reps, fold_regs: 8, break_rotl: false, diff_states: 0, verify: false };
let (mut dag, regs, _) = analyse_block(&block, &opts);
let (cost_a, _, _, _) = dag.realise(&regs, false);
let need = dag.last_need.clone();
let choice = dag.last_choice.clone();
let own = dag.last_cost.clone();
let (cost_c, _, _, _) = dag.realise(&regs, true);
let need_c = dag.last_need.clone();
let own_c = dag.last_cost.clone();
println!("seed {seed} reps {reps}: instructions {} -> {cost_a}; chip {} -> {cost_c}", block.len() * reps, block.iter().filter(|i| i.op != Op::Rotl).count() * reps);
println!("idx op d a b rot mask | node new needed opt cost(A) cost(C) | normal form");
let mut saved_lines = 0;
for (k, (id, new)) in dag.trace.iter().enumerate() {
let ins = &block[k % block.len()];
let i = *id as usize;
let needed = need[i];
let opt = if !needed { "-" } else if choice[i] == 1 { "a" } else if choice[i] == 2 { "b" } else { "?" };
let ca = if needed { own[i] } else { 0 };
let cc = if need_c[i] { own_c[i] } else { 0 };
let flag = if !*new || !needed { "*" } else { " " };
if flag == "*" {
saved_lines += 1;
}
println!(
"{k:4} {flag}{:<6} r{} r{} r{} {:>3} {:>4} | #{:<7} {:<4} {:<6} {:<3} {:<7} {:<7} | {}",
ins.op.name(),
ins.dst,
ins.src,
ins.src2,
ins.rot,
ins.mask,
id,
if *new { "new" } else { "same" },
if needed { "yes" } else { "no" },
opt,
ca,
cc,
node_text(&dag, *id)
);
}
println!("lines flagged * (result is an existing node, or a node the realisation does not need): {saved_lines}");
// the needed nodes that no instruction produced (atoms interned by option a)
let produced: std::collections::HashSet<Id> = dag.trace.iter().map(|t| t.0).collect();
for i in 0..dag.nodes.len() {
if need[i] && !produced.contains(&(i as Id)) && own[i] > 0 {
println!("extra needed node #{i} cost {} : {}", own[i], node_text(&dag, i as Id));
}
}
}
fn emit_c(args: &[String]) {
let seed = arg(args, "--seed").unwrap_or_else(|| "attack-f1/0".into());
let out = arg(args, "--out").expect("--out");
let p = program_for(&seed);
let mut s = String::new();
s.push_str("#include <stdint.h>\n");
s.push_str("uint32_t shfl(uint32_t v, uint32_t mask) __attribute__((const));\n");
s.push_str("static inline uint32_t rotl_imm(uint32_t x, unsigned n) { return (x << n) | (x >> (32u - n)); }\n");
s.push_str("static inline uint32_t rotr_var(uint32_t x, uint32_t s) { s &= 31u; return s ? ((x >> s) | (x << (32u - s))) : x; }\n");
s.push_str("static inline uint32_t mulhi(uint32_t a, uint32_t b) { return (uint32_t)(((uint64_t)a * (uint64_t)b) >> 32); }\n");
s.push_str("void pass(uint32_t *r, uint32_t sel) {\n");
for i in 0..8 {
let _ = writeln!(s, " uint32_t r{i} = r[{i}];");
}
for ins in &p.shadow {
let d = format!("r{}", ins.dst);
let a = format!("r{}", ins.src);
let b = format!("r{}", ins.src2);
let line = match ins.op {
Op::Add => format!("{d} = {d} + {a} + ((((sel >> {}u) & 1u) != 0u) ? 0x{:08x}u : 0x{:08x}u);", ins.bit, ins.imm2, ins.imm),
Op::Sub => format!("{d} = {d} - {a};"),
Op::Mul => format!("{d} = {d} * {a};"),
Op::MulHi => format!("{d} = mulhi({d}, {a});"),
Op::Xor => format!("{d} = {d} ^ {a};"),
Op::Or => format!("{d} = {d} | {a};"),
Op::Rotl => format!("{d} = rotl_imm({d}, {}u);", ins.rot),
Op::Rotr => format!("{d} = rotr_var({d}, {a});"),
Op::Mad => format!("{d} = {a} * {b} + {d};"),
Op::Shfl => format!("{d} = {d} ^ shfl({a}, {}u);", ins.mask),
_ => unreachable!(),
};
let _ = writeln!(s, " {line}");
}
for i in 0..8 {
let _ = writeln!(s, " r[{i}] = r{i};");
}
s.push_str("}\n");
std::fs::write(&out, s).expect("write c");
eprintln!("wrote {out}");
}
fn census(args: &[String]) {
let count: usize = arg(args, "--count").map(|s| s.parse().unwrap()).unwrap_or(10_000);
let threads: usize = arg(args, "--threads").map(|s| s.parse().unwrap()).unwrap_or(12);
let out = arg(args, "--out").unwrap_or_else(|| ".".into());
let prefix = arg(args, "--prefix").unwrap_or_else(|| "attack-f1/".into());
let diff_states: usize = arg(args, "--diff-states").map(|s| s.parse().unwrap()).unwrap_or(8);
let verify = !args.iter().any(|a| a == "--no-verify");
std::fs::create_dir_all(&out).expect("out dir");
let t0 = std::time::Instant::now();
let results: Vec<Report> = std::thread::scope(|s| {
let mut hs = Vec::new();
for t in 0..threads {
let prefix = prefix.clone();
hs.push(s.spawn(move || {
let ds = DatasetSource::new("attack-f1", DatasetMode::ClosedForm, 20);
let opts = Opts { reps: REPS, fold_regs: 8, break_rotl: false, diff_states, verify };
let mut v = Vec::new();
let mut i = t;
while i < count {
let seed = format!("{prefix}{i}");
let p = program_for(&seed);
v.push(analyse(i, &seed, &p, &p.shadow, &opts, Some(&ds)));
i += threads;
}
v
}));
}
let mut all: Vec<Report> = hs.into_iter().flat_map(|h| h.join().unwrap()).collect();
all.sort_by_key(|r| r.idx);
all
});
let secs = t0.elapsed().as_secs_f64();
let mut f = std::fs::File::create(format!("{out}/census.csv")).expect("csv");
writeln!(f, "{CSV_HEADER}").unwrap();
for r in &results {
writeln!(f, "{}", r.csv()).unwrap();
}
let n = results.len() as f64;
let saves: Vec<f64> = results.iter().map(|r| r.save_a()).collect();
let savec: Vec<f64> = results.iter().map(|r| r.save_c()).collect();
let max_a = saves.iter().cloned().fold(f64::MIN, f64::max);
let min_a = saves.iter().cloned().fold(f64::MAX, f64::min);
let mean_a = saves.iter().sum::<f64>() / n;
let max_c = savec.iter().cloned().fold(f64::MIN, f64::max);
let mean_c = savec.iter().sum::<f64>() / n;
let over5 = saves.iter().filter(|&&s| s > 5.0).count();
let over10 = saves.iter().filter(|&&s| s > 10.0).count();
let diff_fail = results.iter().filter(|r| !r.diff_ok).count();
let ver_fail = results.iter().filter(|r| r.verify_ok == Some(false)).count();
let ver_run = results.iter().filter(|r| r.verify_ok.is_some()).count();
let worst = results.iter().max_by(|a, b| a.save_a().partial_cmp(&b.save_a()).unwrap()).unwrap();
let dead = results.iter().map(|r| r.unneeded_derived).sum::<usize>();
let rw = results.iter().fold(Rewrites::default(), |acc, r| Rewrites {
identity: acc.identity + r.rw.identity,
xor_cancel: acc.xor_cancel + r.rw.xor_cancel,
sum_cancel: acc.sum_cancel + r.rw.sum_cancel,
or_idem: acc.or_idem + r.rw.or_idem,
rotl_merge: acc.rotl_merge + r.rw.rotl_merge,
rotr_merge: acc.rotr_merge + r.rw.rotr_merge,
mul_shared: acc.mul_shared + r.rw.mul_shared,
});
let mut hist = [0usize; 12];
for &s in &saves {
let b = ((s / 0.5).floor() as usize).min(11);
hist[b] += 1;
}
let mut summary = String::new();
let _ = writeln!(summary, "attack-f1 census: {} programs ({}{{0..{}}}), {} threads, {:.1} s", results.len(), prefix, count - 1, threads, secs);
let _ = writeln!(summary, "naive per iteration: {} instructions ({} per hash), {} counted ops ({} per hash, the 1.83 convention), chip view {} ops", results[0].naive_a, results[0].naive_a * ITERATIONS, results[0].naive_ops, results[0].naive_ops * ITERATIONS, results[0].naive_c);
let _ = writeln!(summary, "instructions saved: min {:.3}% mean {:.3}% max {:.3}% (worst seed {} idx {}: {} -> {})", min_a, mean_a, max_a, worst.seed, worst.idx, worst.naive_a, worst.cost_a);
let _ = writeln!(summary, "chip-view ops saved beyond free rotates and hoisted constants: mean {:.3}% max {:.3}%", mean_c, max_c);
let _ = writeln!(summary, "programs over 5%: {over5}; over 10%: {over10}; gate (every program within 5%, none over 10%): {}", if over5 == 0 && over10 == 0 { "PASS" } else { "FAIL" });
let _ = writeln!(summary, "soundness: differential mismatches {diff_fail} of {} ({} random states each); verifier mismatches {ver_fail} of {ver_run}", results.len(), diff_states);
let _ = writeln!(summary, "dead (never-read) derived nodes under the full fold: {dead}");
let _ = writeln!(summary, "rewrites over all programs and 27 passes: identity {} xor-cancel {} sum-cancel {} or-idem {} rotl-merge {} rotr-merge {} product-shared {}", rw.identity, rw.xor_cancel, rw.sum_cancel, rw.or_idem, rw.rotl_merge, rw.rotr_merge, rw.mul_shared);
let _ = writeln!(summary, "histogram of instructions saved, 0.5% bins from 0: {:?} (last bin = 5.5% and over)", hist);
print!("{summary}");
std::fs::write(format!("{out}/summary.txt"), &summary).expect("summary");
}