1498 lines
60 KiB
Rust
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, ®s),
|
|
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(®s, false);
|
|
let need = dag.last_need.clone();
|
|
let choice = dag.last_choice.clone();
|
|
let own = dag.last_cost.clone();
|
|
let (cost_c, _, _, _) = dag.realise(®s, 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");
|
|
}
|