diff --git a/tools/attack/adv-mixer-3/Cargo.lock b/tools/attack/adv-mixer-3/Cargo.lock new file mode 100644 index 000000000..d7c7da941 --- /dev/null +++ b/tools/attack/adv-mixer-3/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adv-mixer-3" +version = "0.1.0" +dependencies = [ + "igneum-pow", +] + +[[package]] +name = "igneum-pow" +version = "0.2.0" diff --git a/tools/attack/adv-mixer-3/Cargo.toml b/tools/attack/adv-mixer-3/Cargo.toml new file mode 100644 index 000000000..2e55cdba4 --- /dev/null +++ b/tools/attack/adv-mixer-3/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "adv-mixer-3" +version = "0.1.0" +edition = "2021" +description = "Adversarial lane adv-mixer-3: the statistical distinguisher and the round margin of the mixer M_r (index census, avalanche, differential, linear, rotational-XOR, SAT model)" +license = "MIT" +publish = false + +[[bin]] +name = "adv-mixer-3" +path = "src/main.rs" + +[dependencies] +igneum-pow = { path = "../../../igneum-pow" } + +[workspace] + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 diff --git a/tools/attack/adv-mixer-3/queue/01-adv-mixer-3-index-20729.sh b/tools/attack/adv-mixer-3/queue/01-adv-mixer-3-index-20729.sh new file mode 100644 index 000000000..8a9604453 --- /dev/null +++ b/tools/attack/adv-mixer-3/queue/01-adv-mixer-3-index-20729.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# adv-mixer-3 queue file (owner: adv-mixer-3). Self-contained: binary, args, logs. Runs at nice 10 on cores 8-95 via run-box.sh. +set -uo pipefail +X=/srv/builds/_adv-mixer-3/bin/adv-mixer-3; L=/srv/builds/_adv-mixer-3/logs; mkdir -p "$L" +echo "start $(hostname) $(date -u +%FT%TZ) $(sha256sum $X)" +for k in 0 1 2 3 4 5 6 7 8; do + echo "== index day 20729 k=$k $(date -u +%FT%TZ)" + $X index --day 20729 --apps $k --threads 64 > "$L/index-20729-k$k.log" 2>&1; echo "rc=$?"; grep -E 'chi2=|verdict' "$L/index-20729-k$k.log" +done +echo "== index nomul plant k=1"; $X index --day 20729 --apps 1 --plant nomul --threads 64 > "$L/index-20729-nomul-k1.log" 2>&1; grep -E 'chi2=|verdict' "$L/index-20729-nomul-k1.log" +echo "end $(date -u +%FT%TZ)" diff --git a/tools/attack/adv-mixer-3/queue/02-adv-mixer-3-index-20733.sh b/tools/attack/adv-mixer-3/queue/02-adv-mixer-3-index-20733.sh new file mode 100644 index 000000000..a664916d6 --- /dev/null +++ b/tools/attack/adv-mixer-3/queue/02-adv-mixer-3-index-20733.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# adv-mixer-3 queue file (owner: adv-mixer-3). Self-contained: binary, args, logs. Runs at nice 10 on cores 8-95 via run-box.sh. +set -uo pipefail +X=/srv/builds/_adv-mixer-3/bin/adv-mixer-3; L=/srv/builds/_adv-mixer-3/logs; mkdir -p "$L" +echo "start $(hostname) $(date -u +%FT%TZ) $(sha256sum $X)" +for k in 0 1 2 3 4 5 6 7 8; do + echo "== index day 20733 k=$k $(date -u +%FT%TZ)" + $X index --day 20733 --apps $k --threads 64 > "$L/index-20733-k$k.log" 2>&1; echo "rc=$?"; grep -E 'chi2=|verdict' "$L/index-20733-k$k.log" +done +echo "== index nomul plant k=1"; $X index --day 20733 --apps 1 --plant nomul --threads 64 > "$L/index-20733-nomul-k1.log" 2>&1; grep -E 'chi2=|verdict' "$L/index-20733-nomul-k1.log" +echo "end $(date -u +%FT%TZ)" diff --git a/tools/attack/adv-mixer-3/queue/03-adv-mixer-3-sac-20729.sh b/tools/attack/adv-mixer-3/queue/03-adv-mixer-3-sac-20729.sh new file mode 100644 index 000000000..82a81571e --- /dev/null +++ b/tools/attack/adv-mixer-3/queue/03-adv-mixer-3-sac-20729.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# adv-mixer-3 queue file (owner: adv-mixer-3). Self-contained: binary, args, logs. Runs at nice 10 on cores 8-95 via run-box.sh. +set -uo pipefail +X=/srv/builds/_adv-mixer-3/bin/adv-mixer-3; L=/srv/builds/_adv-mixer-3/logs; mkdir -p "$L" +echo "start $(hostname) $(date -u +%FT%TZ) $(sha256sum $X)" +echo "== sac plant weak k=8"; $X sac --day 20729 --apps 8 --plant weak --states 65536 --threads 64 > "$L/sac-20729-weak-k8.log" 2>&1; grep -E 'holes=|verdict' "$L/sac-20729-weak-k8.log" +echo "== sac plant one k=8"; $X sac --day 20729 --apps 8 --plant one --states 65536 --threads 64 > "$L/sac-20729-one-k8.log" 2>&1; grep -E 'holes=|verdict' "$L/sac-20729-one-k8.log" +for k in 1 2 3 4 5 6 7 8; do + echo "== sac day 20729 k=$k states 2^24 $(date -u +%FT%TZ)" + $X sac --day 20729 --apps $k --states 2^24 --threads 64 > "$L/sac-20729-k$k.log" 2>&1; echo "rc=$?"; grep -E 'holes=|line index|verdict' "$L/sac-20729-k$k.log" +done +for k in 2 3 4; do + echo "== sac day 20729 k=$k states 2^27 $(date -u +%FT%TZ)" + $X sac --day 20729 --apps $k --states 2^27 --threads 64 > "$L/sac-20729-k$k-n27.log" 2>&1; echo "rc=$?"; grep -E 'holes=|line index|verdict' "$L/sac-20729-k$k-n27.log" +done +echo "end $(date -u +%FT%TZ)" diff --git a/tools/attack/adv-mixer-3/queue/04-adv-mixer-3-sac-20733.sh b/tools/attack/adv-mixer-3/queue/04-adv-mixer-3-sac-20733.sh new file mode 100644 index 000000000..24ffd5ab6 --- /dev/null +++ b/tools/attack/adv-mixer-3/queue/04-adv-mixer-3-sac-20733.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# adv-mixer-3 queue file (owner: adv-mixer-3). Self-contained: binary, args, logs. Runs at nice 10 on cores 8-95 via run-box.sh. +set -uo pipefail +X=/srv/builds/_adv-mixer-3/bin/adv-mixer-3; L=/srv/builds/_adv-mixer-3/logs; mkdir -p "$L" +echo "start $(hostname) $(date -u +%FT%TZ) $(sha256sum $X)" +echo "== sac plant weak k=8"; $X sac --day 20733 --apps 8 --plant weak --states 65536 --threads 64 > "$L/sac-20733-weak-k8.log" 2>&1; grep -E 'holes=|verdict' "$L/sac-20733-weak-k8.log" +echo "== sac plant one k=8"; $X sac --day 20733 --apps 8 --plant one --states 65536 --threads 64 > "$L/sac-20733-one-k8.log" 2>&1; grep -E 'holes=|verdict' "$L/sac-20733-one-k8.log" +for k in 1 2 3 4 5 6 7 8; do + echo "== sac day 20733 k=$k states 2^24 $(date -u +%FT%TZ)" + $X sac --day 20733 --apps $k --states 2^24 --threads 64 > "$L/sac-20733-k$k.log" 2>&1; echo "rc=$?"; grep -E 'holes=|line index|verdict' "$L/sac-20733-k$k.log" +done +for k in 2 3 4; do + echo "== sac day 20733 k=$k states 2^27 $(date -u +%FT%TZ)" + $X sac --day 20733 --apps $k --states 2^27 --threads 64 > "$L/sac-20733-k$k-n27.log" 2>&1; echo "rc=$?"; grep -E 'holes=|line index|verdict' "$L/sac-20733-k$k-n27.log" +done +echo "end $(date -u +%FT%TZ)" diff --git a/tools/attack/adv-mixer-3/queue/05-adv-mixer-3-difflinrx-20729.sh b/tools/attack/adv-mixer-3/queue/05-adv-mixer-3-difflinrx-20729.sh new file mode 100644 index 000000000..80f2d32d0 --- /dev/null +++ b/tools/attack/adv-mixer-3/queue/05-adv-mixer-3-difflinrx-20729.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# adv-mixer-3 queue file (owner: adv-mixer-3). Self-contained: binary, args, logs. Runs at nice 10 on cores 8-95 via run-box.sh. +set -uo pipefail +X=/srv/builds/_adv-mixer-3/bin/adv-mixer-3; L=/srv/builds/_adv-mixer-3/logs; mkdir -p "$L" +echo "start $(hostname) $(date -u +%FT%TZ) $(sha256sum $X)" +echo "== diff plant nomul k=1"; $X diff --day 20729 --apps 1 --plant nomul --samples 65536 --threads 64 > "$L/diff-20729-nomul-k1.log" 2>&1; grep -E 'max mult|determin|verdict' "$L/diff-20729-nomul-k1.log" +for k in 1 2 3 4 5 6 7 8; do + echo "== diff day 20729 k=$k $(date -u +%FT%TZ)" + $X diff --day 20729 --apps $k --samples 2^20 --threads 64 > "$L/diff-20729-k$k.log" 2>&1; echo "rc=$?"; grep -E 'max mult|determin|zero out|verdict' "$L/diff-20729-k$k.log" +done +echo "== lin identity plant k=0"; $X lin --day 20729 --apps 0 --samples 2^20 --threads 64 > "$L/lin-20729-k0.log" 2>&1; grep -E 'worst|verdict' "$L/lin-20729-k0.log" +for k in 1 2 3 4 5 6 7 8; do + echo "== lin day 20729 k=$k $(date -u +%FT%TZ)" + $X lin --day 20729 --apps $k --samples 2^26 --threads 64 > "$L/lin-20729-k$k.log" 2>&1; echo "rc=$?"; grep -E 'worst|verdict' "$L/lin-20729-k$k.log" +done +echo "== lin0 identity-ish plant k=0"; $X lin0 --day 20729 --apps 0 --samples 2^20 --threads 64 > "$L/lin0-20729-k0.log" 2>&1; grep -E 'worst|verdict' "$L/lin0-20729-k0.log" +for k in 1 2 3 4 5 6 7 8; do + echo "== lin0 day 20729 k=$k $(date -u +%FT%TZ)" + $X lin0 --day 20729 --apps $k --samples 2^26 --threads 64 > "$L/lin0-20729-k$k.log" 2>&1; echo "rc=$?"; grep -E 'worst|verdict' "$L/lin0-20729-k$k.log" +done +echo "== rx plant weak0 k=1"; $X rx --day 20729 --apps 1 --plant weak0 --samples 2^20 > "$L/rx-20729-weak0-k1.log" 2>&1; grep -E 'rx r=|verdict' "$L/rx-20729-weak0-k1.log" +for k in 1 2 3 4; do + echo "== rx day 20729 k=$k $(date -u +%FT%TZ)" + $X rx --day 20729 --apps $k --samples 2^22 > "$L/rx-20729-k$k.log" 2>&1; echo "rc=$?"; grep -E 'rx r=|verdict' "$L/rx-20729-k$k.log" +done +echo "end $(date -u +%FT%TZ)" diff --git a/tools/attack/adv-mixer-3/queue/06-adv-mixer-3-difflinrx-20733.sh b/tools/attack/adv-mixer-3/queue/06-adv-mixer-3-difflinrx-20733.sh new file mode 100644 index 000000000..70a04a0b3 --- /dev/null +++ b/tools/attack/adv-mixer-3/queue/06-adv-mixer-3-difflinrx-20733.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# adv-mixer-3 queue file (owner: adv-mixer-3). Self-contained: binary, args, logs. Runs at nice 10 on cores 8-95 via run-box.sh. +set -uo pipefail +X=/srv/builds/_adv-mixer-3/bin/adv-mixer-3; L=/srv/builds/_adv-mixer-3/logs; mkdir -p "$L" +echo "start $(hostname) $(date -u +%FT%TZ) $(sha256sum $X)" +echo "== diff plant nomul k=1"; $X diff --day 20733 --apps 1 --plant nomul --samples 65536 --threads 64 > "$L/diff-20733-nomul-k1.log" 2>&1; grep -E 'max mult|determin|verdict' "$L/diff-20733-nomul-k1.log" +for k in 1 2 3 4 5 6 7 8; do + echo "== diff day 20733 k=$k $(date -u +%FT%TZ)" + $X diff --day 20733 --apps $k --samples 2^20 --threads 64 > "$L/diff-20733-k$k.log" 2>&1; echo "rc=$?"; grep -E 'max mult|determin|zero out|verdict' "$L/diff-20733-k$k.log" +done +echo "== lin identity plant k=0"; $X lin --day 20733 --apps 0 --samples 2^20 --threads 64 > "$L/lin-20733-k0.log" 2>&1; grep -E 'worst|verdict' "$L/lin-20733-k0.log" +for k in 1 2 3 4 5 6 7 8; do + echo "== lin day 20733 k=$k $(date -u +%FT%TZ)" + $X lin --day 20733 --apps $k --samples 2^26 --threads 64 > "$L/lin-20733-k$k.log" 2>&1; echo "rc=$?"; grep -E 'worst|verdict' "$L/lin-20733-k$k.log" +done +echo "== lin0 identity-ish plant k=0"; $X lin0 --day 20733 --apps 0 --samples 2^20 --threads 64 > "$L/lin0-20733-k0.log" 2>&1; grep -E 'worst|verdict' "$L/lin0-20733-k0.log" +for k in 1 2 3 4 5 6 7 8; do + echo "== lin0 day 20733 k=$k $(date -u +%FT%TZ)" + $X lin0 --day 20733 --apps $k --samples 2^26 --threads 64 > "$L/lin0-20733-k$k.log" 2>&1; echo "rc=$?"; grep -E 'worst|verdict' "$L/lin0-20733-k$k.log" +done +echo "== rx plant weak0 k=1"; $X rx --day 20733 --apps 1 --plant weak0 --samples 2^20 > "$L/rx-20733-weak0-k1.log" 2>&1; grep -E 'rx r=|verdict' "$L/rx-20733-weak0-k1.log" +for k in 1 2 3 4; do + echo "== rx day 20733 k=$k $(date -u +%FT%TZ)" + $X rx --day 20733 --apps $k --samples 2^22 > "$L/rx-20733-k$k.log" 2>&1; echo "rc=$?"; grep -E 'rx r=|verdict' "$L/rx-20733-k$k.log" +done +echo "end $(date -u +%FT%TZ)" diff --git a/tools/attack/adv-mixer-3/queue/07-adv-mixer-3-days.sh b/tools/attack/adv-mixer-3/queue/07-adv-mixer-3-days.sh new file mode 100644 index 000000000..f8fff45fc --- /dev/null +++ b/tools/attack/adv-mixer-3/queue/07-adv-mixer-3-days.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# adv-mixer-3 queue file (owner: adv-mixer-3). Self-contained: binary, args, logs. Runs at nice 10 on cores 8-95 via run-box.sh. +set -uo pipefail +X=/srv/builds/_adv-mixer-3/bin/adv-mixer-3; L=/srv/builds/_adv-mixer-3/logs; mkdir -p "$L" +echo "start $(hostname) $(date -u +%FT%TZ) $(sha256sum $X)" +# eight day indices drawn from SplitMix64(0x6164762d6d697833) mod 100000 + 20729, fixed here +for d in 20730 20745 21057 23311 27406 33333 50000 99999; do + for k in 2 3 4 8; do + echo "== index day $d k=$k $(date -u +%FT%TZ)" + $X index --day $d --apps $k --threads 64 > "$L/index-$d-k$k.log" 2>&1; grep -E 'chi2=|verdict' "$L/index-$d-k$k.log" + done + for k in 2 3 4; do + echo "== sac day $d k=$k $(date -u +%FT%TZ)" + $X sac --day $d --apps $k --states 2^24 --threads 64 > "$L/sac-$d-k$k.log" 2>&1; grep -E 'holes=|verdict' "$L/sac-$d-k$k.log" + done +done +echo "end $(date -u +%FT%TZ)" diff --git a/tools/attack/adv-mixer-3/queue/08-adv-mixer-3-sat.sh b/tools/attack/adv-mixer-3/queue/08-adv-mixer-3-sat.sh new file mode 100644 index 000000000..a6287a027 --- /dev/null +++ b/tools/attack/adv-mixer-3/queue/08-adv-mixer-3-sat.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# adv-mixer-3 queue file (owner: adv-mixer-3). Self-contained: binary, args, logs. Runs at nice 10 on cores 8-95 via run-box.sh. +set -uo pipefail +X=/srv/builds/_adv-mixer-3/bin/adv-mixer-3; L=/srv/builds/_adv-mixer-3/logs; mkdir -p "$L" +echo "start $(hostname) $(date -u +%FT%TZ) $(sha256sum $X)" +C=/srv/builds/_adv-mixer-3/cnf; S=$HOME/adv-mixer-3-tools/cadical/build/cadical +for d in 20729 20733; do for k in 1 2 3 4; do + echo "== cnf day $d k=$k $(date -u +%FT%TZ)" + $X cnf --day $d --apps $k --t0 0x12345678 --out "$C/d$d-k$k.cnf" > "$L/cnf-$d-k$k.log" 2>&1; grep -E 'target|cnf:' "$L/cnf-$d-k$k.log" + /usr/bin/time -f "wall=%e s maxrss=%M KB" timeout 3600 $S -q "$C/d$d-k$k.cnf" > "$L/sat-$d-k$k.log" 2>&1; echo "cadical rc=$? (10 sat, 20 unsat, 124 timeout)" + grep -E '^s |wall=' "$L/sat-$d-k$k.log" + if grep -q '^s SATISFIABLE' "$L/sat-$d-k$k.log"; then T=$($X model --file "$L/sat-$d-k$k.log" | sed 's/.*t=\(0x[0-9a-f]*\).*/\1/'); echo "model $T"; $X verify --day $d --apps $k --t $T; fi +done; done +echo "end $(date -u +%FT%TZ)" diff --git a/tools/attack/adv-mixer-3/run-box.sh b/tools/attack/adv-mixer-3/run-box.sh new file mode 100755 index 000000000..50802e6d8 --- /dev/null +++ b/tools/attack/adv-mixer-3/run-box.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# adv-mixer-3 runner (box side). Launches one queue file detached at nice 10 on cores 8 to 95 (cores 0 to 7 are +# reserved for release builds, the seed and the observer; build-server lane rule of 7 October 2026, 19:4x BST, which +# also dropped the SIGSTOP yield to builds: the scheduler arbitrates). Log and pid file live OUTSIDE the worktree +# mirror, under /srv/builds/_adv-mixer-3/logs/ (the mirror is wiped by every source sync). Kill by pid file only: +# `kill -- -$(cat )` (setsid makes the script its own process group). +# +# run-box.sh launch; prints the pid and the log path +set -euo pipefail +Q="$1"; B=$(basename "$Q" .sh) +ROOT=/srv/builds/_adv-mixer-3; mkdir -p "$ROOT/logs" "$ROOT/cnf" +LOG="$ROOT/logs/$B.log"; PID="$ROOT/logs/$B.pid" +if [ -f "$PID" ] && kill -0 "$(cat "$PID")" 2>/dev/null; then echo "already running: pid $(cat "$PID")"; exit 1; fi +nohup setsid nice -n 10 taskset -c 8-95 bash "$Q" > "$LOG" 2>&1 & +echo $! > "$PID" +echo "launched $B pid $(cat "$PID") log $LOG host $(hostname) utc $(date -u +%FT%TZ)" diff --git a/tools/attack/adv-mixer-3/src/main.rs b/tools/attack/adv-mixer-3/src/main.rs new file mode 100644 index 000000000..a7cfbaa61 --- /dev/null +++ b/tools/attack/adv-mixer-3/src/main.rs @@ -0,0 +1,1021 @@ +//! Adversarial lane adv-mixer-3: the statistical distinguisher and the round margin of the mixer `M_r` +//! (`igneum_pow::memhard::mixer`, spec 01 section 1.8.4), class v4 (`mixer_mult = 8`: 72 applications per item, 8 +//! between dependent cache reads). Internal adversarial pass, not an independent review. +//! +//! Every command takes `--day ` (the key is `seed_words_from_bytes(bind::day_bytes(day))`, the draw +//! `MixParams::with_shape` under `Shape::for_class(&V4_CLASS)`), `--apps k` (applications), `--start a` (the first +//! application's index into the 72 round keys, default 0 for the round-0 commands and 8 otherwise) and +//! `--plant none|one|nomul|weak|weak0` (the known-failed shapes: `one` runs one application in place of k, `nomul` +//! sets every MUL to 1, `weak` is MUL 1 / RC 0 / ROT 16, `weak0` is `weak` with every round key 0). +//! +//! adv-mixer-3 index --day D --apps k --threads T exhaustive line index census over all 2^32 t (round 0) +//! adv-mixer-3 sac --day D --apps k --states N --threads T single-bit avalanche matrix on random states +//! adv-mixer-3 diff --day D --apps k --samples N --threads T differential multiplicity for low-weight input differences +//! adv-mixer-3 lin --day D --apps k --samples N --threads T single-bit linear correlations on random states +//! adv-mixer-3 lin0 --day D --apps k --samples N --threads T t-bit to state-bit correlations on the round-0 input +//! adv-mixer-3 rx --day D --apps k --samples N rotational-XOR properties +//! adv-mixer-3 cnf --day D --apps k --t0 T --out file a DIMACS CNF: find t whose index after k applications is t0's +//! adv-mixer-3 verify --day D --apps k --t T the index of t after k applications through the real code +//! adv-mixer-3 params --day D the day's draw + +use igneum_pow::bind::day_bytes; +use igneum_pow::generator::V4_CLASS; +use igneum_pow::memhard::{mixer, round_key, MixParams, Shape, ITEM_ROUNDS}; +use igneum_pow::seed::{seed_words_from_bytes, SplitMix64}; +use std::collections::HashMap; +use std::io::Write as _; +use std::time::Instant; + +const LINE_MASK: u32 = (1u32 << 22) - 1; +const APPS_PER_ITEM: usize = (ITEM_ROUNDS + 1) * 8; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Plant { + None, + One, + NoMul, + Weak, + Weak0, +} + +impl Plant { + fn parse(s: &str) -> Plant { + match s { + "none" => Plant::None, + "one" => Plant::One, + "nomul" => Plant::NoMul, + "weak" => Plant::Weak, + "weak0" => Plant::Weak0, + _ => panic!("unknown plant {s}"), + } + } +} + +struct Args { + cmd: String, + kv: HashMap, +} + +impl Args { + fn parse() -> Args { + let mut it = std::env::args().skip(1); + let cmd = it.next().unwrap_or_else(|| "help".into()); + let mut kv = HashMap::new(); + while let Some(k) = it.next() { + let k = k.trim_start_matches("--").to_string(); + let v = it.next().unwrap_or_default(); + kv.insert(k, v); + } + Args { cmd, kv } + } + fn u64(&self, k: &str, d: u64) -> u64 { + self.kv.get(k).map(|v| parse_num(v)).unwrap_or(d) + } + fn usize(&self, k: &str, d: usize) -> usize { + self.u64(k, d as u64) as usize + } + fn str(&self, k: &str, d: &str) -> String { + self.kv.get(k).cloned().unwrap_or_else(|| d.to_string()) + } +} + +/// `2^20`, `0x..` or decimal. +fn parse_num(v: &str) -> u64 { + if let Some(e) = v.strip_prefix("2^") { + 1u64 << e.parse::().unwrap() + } else if let Some(h) = v.strip_prefix("0x") { + u64::from_str_radix(h, 16).unwrap() + } else { + v.parse().unwrap() + } +} + +fn v4_shape() -> Shape { + let s = Shape::for_class(&V4_CLASS); + assert_eq!(s.mixer_mult, 8, "class v4 is the x8 mixer"); + assert_eq!(s.derive_len, 0, "class v4 has no derivation program"); + s +} + +/// The chain day's key and draw through the real code, then the plant. +fn params_of_day(day: u64, plant: Plant) -> MixParams { + let mut mp = MixParams::with_shape(seed_words_from_bytes(&day_bytes(day)), v4_shape()); + match plant { + Plant::None | Plant::One => {} + Plant::NoMul => mp.mul = [1; 16], + Plant::Weak | Plant::Weak0 => { + mp.mul = [1; 16]; + mp.rc = [0; 16]; + mp.rot = [16; 8]; + } + } + mp +} + +/// The round keys of applications `start .. start + k` (the item's 72 keys, `round_key(start + j)`), zero under weak0. +fn keys(start: usize, k: usize, plant: Plant) -> Vec { + assert!(start + k <= APPS_PER_ITEM || plant != Plant::None, "an item has 72 applications"); + (0..k).map(|j| if plant == Plant::Weak0 { 0 } else { round_key(start + j) }).collect() +} + +#[inline(always)] +fn apply(s: &mut [u32; 16], ks: &[u32], mp: &MixParams, plant: Plant) { + if plant == Plant::One { + if let Some(&k) = ks.first() { + mixer(s, k, mp); + } + return; + } + for &k in ks { + mixer(s, k, mp); + } +} + +/// The round-0 init of item `t` (`derive_items_mask`). +#[inline(always)] +fn init(t: u32, mp: &MixParams) -> [u32; 16] { + let mut s = [0u32; 16]; + s[..8].copy_from_slice(&mp.key); + for i in 0..8 { + s[8 + i] = t.wrapping_mul(mp.mul[i]).wrapping_add(mp.rc[i]); + } + s +} + +fn random_state(rng: &mut SplitMix64) -> [u32; 16] { + let mut s = [0u32; 16]; + for i in 0..8 { + let v = rng.next(); + s[2 * i] = v as u32; + s[2 * i + 1] = (v >> 32) as u32; + } + s +} + +fn seed_thread(day: u64, salt: u64, th: u64) -> SplitMix64 { + SplitMix64::new(day.wrapping_mul(0x9E3779B97F4A7C15) ^ salt.wrapping_mul(0xD1B54A32D192ED03) ^ (th + 1).wrapping_mul(0x8CB92BA72F3D8DD7)) +} + +fn now() -> String { + let t = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + format!("{t}") +} + +fn header(a: &Args, mp: &MixParams, plant: Plant, start: usize, k: usize) { + println!("adv-mixer-3 {} day={} apps={} start={} plant={:?} unix={}", a.cmd, a.u64("day", 20729), k, start, plant, now()); + println!("key = {}", mp.key.iter().map(|w| format!("{w:08x}")).collect::>().join(" ")); + println!("rot = {:?}", mp.rot); + println!("mul = {}", mp.mul.iter().map(|w| format!("{w:08x}")).collect::>().join(" ")); + println!("rc = {}", mp.rc.iter().map(|w| format!("{w:08x}")).collect::>().join(" ")); + println!("keys = {}", keys(start, k, plant).iter().map(|w| format!("{w:08x}")).collect::>().join(" ")); +} + +// ------------------------------------------------------------------------------------------------------------------ +// index: the exhaustive round-0 line index census +// ------------------------------------------------------------------------------------------------------------------ + +fn cmd_index(a: &Args) { + let day = a.u64("day", 20729); + let plant = Plant::parse(&a.str("plant", "none")); + let k = a.usize("apps", 8); + let start = a.usize("start", 0); + let threads = a.usize("threads", 32); + let mp = params_of_day(day, plant); + header(a, &mp, plant, start, k); + let ks = keys(start, k, plant); + let t0 = Instant::now(); + let n_bins = 1usize << 22; + // per-thread: 2^22 u32 bins, 32 bit counters for s[0], 512 bit counters on the t & 0xff == 0 sample + let results: Vec<(Vec, [u64; 32], [u64; 512])> = std::thread::scope(|sc| { + let hs: Vec<_> = (0..threads) + .map(|th| { + let mp = ∓ + let ks = &ks; + sc.spawn(move || { + let mut bins = vec![0u32; n_bins]; + let mut bit0 = [0u64; 32]; + let mut bits = [0u64; 512]; + let per = (1u64 << 32) / threads as u64; + let lo = th as u64 * per; + let hi = if th + 1 == threads { 1u64 << 32 } else { lo + per }; + for t in lo..hi { + let mut s = init(t as u32, mp); + apply(&mut s, ks, mp, plant); + bins[(s[0] & LINE_MASK) as usize] += 1; + let w = s[0]; + for b in 0..32 { + bit0[b] += ((w >> b) & 1) as u64; + } + if t & 0xff == 0 { + for i in 0..16 { + for b in 0..32 { + bits[i * 32 + b] += ((s[i] >> b) & 1) as u64; + } + } + } + } + (bins, bit0, bits) + }) + }) + .collect(); + hs.into_iter().map(|h| h.join().unwrap()).collect() + }); + let mut bins = vec![0u64; n_bins]; + let mut bit0 = [0u64; 32]; + let mut bits = [0u64; 512]; + for (b, b0, bb) in &results { + for i in 0..n_bins { + bins[i] += b[i] as u64; + } + for i in 0..32 { + bit0[i] += b0[i]; + } + for i in 0..512 { + bits[i] += bb[i]; + } + } + let n = 1u64 << 32; + let expect = (n / n_bins as u64) as f64; // 1024 + let mut chi2 = 0f64; + let mut empty = 0usize; + let mut min = u64::MAX; + let mut max = 0u64; + let mut argmax = 0usize; + for (i, &c) in bins.iter().enumerate() { + let d = c as f64 - expect; + chi2 += d * d / expect; + if c == 0 { + empty += 1; + } + if c < min { + min = c; + } + if c > max { + max = c; + argmax = i; + } + } + let df = (n_bins - 1) as f64; + let z = (chi2 - df) / (2.0 * df).sqrt(); + // per-segment (high 16 bits) and per-line (low 6 bits) marginals, the structure a verifier's cache layout would feel + let mut seg = vec![0u64; 1 << 16]; + let mut line = [0u64; 64]; + for (i, &c) in bins.iter().enumerate() { + seg[i >> 6] += c; + line[i & 63] += c; + } + let seg_e = (n / (1u64 << 16)) as f64; + let seg_chi: f64 = seg.iter().map(|&c| (c as f64 - seg_e).powi(2) / seg_e).sum(); + let seg_z = (seg_chi - 65535.0) / (2.0 * 65535.0f64).sqrt(); + let line_e = (n / 64) as f64; + let line_chi: f64 = line.iter().map(|&c| (c as f64 - line_e).powi(2) / line_e).sum(); + let line_z = (line_chi - 63.0) / (2.0 * 63.0f64).sqrt(); + println!("census: n=2^32 bins=2^22 expect={expect} wall={:.1}s", t0.elapsed().as_secs_f64()); + println!("chi2={chi2:.1} df={df:.0} z={z:.2} empty={empty} min={min} max={max} argmax={argmax:#x}"); + println!("segments(2^16): chi2={seg_chi:.1} z={seg_z:.2} lines-in-segment(64): chi2={line_chi:.1} z={line_z:.2}"); + // Poisson tail check: the count of bins at or above 1024 + 5 sigma (sigma 32) and at or below 1024 - 5 sigma + let hi5 = bins.iter().filter(|&&c| c >= 1184).count(); + let lo5 = bins.iter().filter(|&&c| c <= 864).count(); + println!("bins >= 1184 (5 sigma high): {hi5} (uniform expects about 1.2); bins <= 864: {lo5} (about 1.2)"); + let sq = (n as f64).sqrt(); + let worst0 = bit0.iter().enumerate().map(|(b, &c)| (b, (2.0 * c as f64 - n as f64) / sq)).fold((0usize, 0f64), |acc, (b, z)| if z.abs() > acc.1.abs() { (b, z) } else { acc }); + println!("s[0] bit balance over 2^32: worst bit {} z={:.2}; low 22 bits worst z={:.2}", worst0.0, worst0.1, bit0[..22].iter().map(|&c| ((2.0 * c as f64 - n as f64) / sq).abs()).fold(0f64, f64::max)); + let ns = (n >> 8) as f64; + let worst = bits.iter().enumerate().map(|(b, &c)| (b, (2.0 * c as f64 - ns) / ns.sqrt())).fold((0usize, 0f64), |acc, (b, z)| if z.abs() > acc.1.abs() { (b, z) } else { acc }); + let beyond6 = bits.iter().filter(|&&c| ((2.0 * c as f64 - ns) / ns.sqrt()).abs() > 6.0).count(); + println!("state bit balance over 2^24 sample: worst bit {} (word {}) z={:.2}; bits beyond 6 sigma: {beyond6} of 512", worst.0, worst.0 / 32, worst.1); + let verdict = if empty > 0 || z.abs() > 6.0 || seg_z.abs() > 6.0 || line_z.abs() > 6.0 || worst0.1.abs() > 6.0 || beyond6 > 0 { "NON-UNIFORM" } else { "uniform within the band" }; + println!("verdict: {verdict}"); +} + +// ------------------------------------------------------------------------------------------------------------------ +// sac: the single-bit avalanche matrix +// ------------------------------------------------------------------------------------------------------------------ + +fn cmd_sac(a: &Args) { + let day = a.u64("day", 20729); + let plant = Plant::parse(&a.str("plant", "none")); + let k = a.usize("apps", 8); + let start = a.usize("start", 8); + let threads = a.usize("threads", 32); + let n = a.u64("states", 1 << 20); + let mp = params_of_day(day, plant); + header(a, &mp, plant, start, k); + let ks = keys(start, k, plant); + let t0 = Instant::now(); + let per = n / threads as u64; + let mats: Vec> = std::thread::scope(|sc| { + let hs: Vec<_> = (0..threads) + .map(|th| { + let mp = ∓ + let ks = &ks; + sc.spawn(move || { + let mut m = vec![0u32; 512 * 512]; + let mut rng = seed_thread(day, 1, th as u64); + let cnt = if th + 1 == threads { n - per * (threads as u64 - 1) } else { per }; + for _ in 0..cnt { + let x = random_state(&mut rng); + let mut y0 = x; + apply(&mut y0, ks, mp, plant); + for i in 0..512 { + let mut xi = x; + xi[i / 32] ^= 1 << (i % 32); + apply(&mut xi, ks, mp, plant); + let row = &mut m[i * 512..(i + 1) * 512]; + for w in 0..16 { + let mut d = xi[w] ^ y0[w]; + while d != 0 { + let b = d.trailing_zeros() as usize; + row[w * 32 + b] += 1; + d &= d - 1; + } + } + } + } + m + }) + }) + .collect(); + hs.into_iter().map(|h| h.join().unwrap()).collect() + }); + let mut m = vec![0u64; 512 * 512]; + for mm in &mats { + for i in 0..m.len() { + m[i] += mm[i] as u64; + } + } + let nf = n as f64; + let sq = nf.sqrt(); + let mut holes = 0usize; + let mut beyond6 = 0usize; + let mut beyond8 = 0usize; + let mut worst = (0usize, 0usize, 0f64); + let mut sum = 0f64; + let mut row_min = vec![f64::MAX; 512]; + for i in 0..512 { + for j in 0..512 { + let c = m[i * 512 + j]; + if c == 0 || c == n { + holes += 1; + } + let p = c as f64 / nf; + let z = (2.0 * p - 1.0) * sq; + sum += p; + if z.abs() > 6.0 { + beyond6 += 1; + } + if z.abs() > 8.0 { + beyond8 += 1; + } + if z.abs() > worst.2.abs() { + worst = (i, j, z); + } + if p < row_min[i] { + row_min[i] = p; + } + } + } + let mean = sum / (512.0 * 512.0); + let worst_p = m[worst.0 * 512 + worst.1] as f64 / nf; + println!("sac: states={n} wall={:.1}s band(6 sigma)={:.5}", t0.elapsed().as_secs_f64(), 6.0 / (2.0 * sq)); + println!("holes={holes} beyond6={beyond6} beyond8={beyond8} mean_flip={mean:.5} worst in={} (word {}) out={} (word {}) p={worst_p:.5} z={:.2}", worst.0, worst.0 / 32, worst.1, worst.1 / 32, worst.2); + // the line index cells: input bits 0..511 against output bits 0..21 (word 0 low 22 bits) + let mut idx_worst = 0f64; + let mut idx_beyond6 = 0usize; + for i in 0..512 { + for j in 0..22 { + let z = (2.0 * m[i * 512 + j] as f64 / nf - 1.0) * sq; + if z.abs() > idx_worst.abs() { + idx_worst = z; + } + if z.abs() > 6.0 { + idx_beyond6 += 1; + } + } + } + println!("line index cells (out bits 0..21): worst z={idx_worst:.2} beyond6={idx_beyond6} of {}", 512 * 22); + let verdict = if holes > 0 || beyond6 > 0 { "DISTINGUISHED" } else { "no cell beyond 6 sigma" }; + println!("verdict: {verdict}"); +} + +// ------------------------------------------------------------------------------------------------------------------ +// diff: differential multiplicity +// ------------------------------------------------------------------------------------------------------------------ + +fn hash_state(s: &[u32; 16]) -> u64 { + let mut h = 0xcbf29ce484222325u64; + for &w in s { + h ^= w as u64; + h = h.wrapping_mul(0x100000001b3); + h ^= h >> 29; + } + h +} + +fn cmd_diff(a: &Args) { + let day = a.u64("day", 20729); + let plant = Plant::parse(&a.str("plant", "none")); + let k = a.usize("apps", 8); + let start = a.usize("start", 8); + let threads = a.usize("threads", 32); + let n = a.usize("samples", 1 << 16); + let mp = params_of_day(day, plant); + header(a, &mp, plant, start, k); + let ks = keys(start, k, plant); + let t0 = Instant::now(); + // input differences: 512 single bits, then 64 random two-bit differences + let mut diffs: Vec<[u32; 16]> = Vec::new(); + for i in 0..512 { + let mut d = [0u32; 16]; + d[i / 32] = 1 << (i % 32); + diffs.push(d); + } + let mut rng = seed_thread(day, 2, 999); + for _ in 0..64 { + let mut d = [0u32; 16]; + let i = (rng.next() % 512) as usize; + let j = (rng.next() % 512) as usize; + d[i / 32] ^= 1 << (i % 32); + d[j / 32] ^= 1 << (j % 32); + diffs.push(d); + } + // per input difference: (max multiplicity of a full output difference, deterministic output bits, max zero words) + let rows: Vec<(usize, usize, usize, usize)> = std::thread::scope(|sc| { + let hs: Vec<_> = (0..threads) + .map(|th| { + let mp = ∓ + let ks = &ks; + let diffs = &diffs; + sc.spawn(move || { + let mut out = Vec::new(); + let mut rng = seed_thread(day, 3, th as u64); + let mut hashes = vec![0u64; n]; + let mut d_idx = th; + while d_idx < diffs.len() { + let dd = diffs[d_idx]; + let mut and = [u32::MAX; 16]; + let mut or = [0u32; 16]; + let mut max_zero_words = 0usize; + let mut zero_word_total = 0usize; + for h in hashes.iter_mut() { + let x = random_state(&mut rng); + let mut y1 = x; + apply(&mut y1, ks, mp, plant); + let mut x2 = x; + for w in 0..16 { + x2[w] ^= dd[w]; + } + apply(&mut x2, ks, mp, plant); + let mut d = [0u32; 16]; + let mut zw = 0usize; + for w in 0..16 { + d[w] = y1[w] ^ x2[w]; + and[w] &= d[w]; + or[w] |= d[w]; + if d[w] == 0 { + zw += 1; + } + } + zero_word_total += zw; + if zw > max_zero_words { + max_zero_words = zw; + } + *h = hash_state(&d); + } + hashes.sort_unstable(); + let mut best = 1usize; + let mut run = 1usize; + for i in 1..n { + if hashes[i] == hashes[i - 1] { + run += 1; + if run > best { + best = run; + } + } else { + run = 1; + } + } + // deterministic output bits: always 1 (and) or always 0 (not or) + let det: usize = (0..16).map(|w| (and[w].count_ones() + (!or[w]).count_ones()) as usize).sum(); + out.push((d_idx, best, det, max_zero_words, zero_word_total)); + d_idx += threads; + } + out + }) + }) + .collect(); + let mut all: Vec<(usize, usize, usize, usize, usize)> = hs.into_iter().flat_map(|h| h.join().unwrap()).collect(); + all.sort(); + all.into_iter().map(|(_, b, d, z, zt)| (b, d, z, zt)).collect() + }); + let max_mult = rows.iter().map(|r| r.0).max().unwrap(); + let arg = rows.iter().position(|r| r.0 == max_mult).unwrap(); + let det_total: usize = rows.iter().map(|r| r.1).sum(); + let det_max = rows.iter().map(|r| r.1).max().unwrap(); + let zero_max = rows.iter().map(|r| r.2).max().unwrap(); + let zero_mean = rows.iter().map(|r| r.3).sum::() as f64 / (rows.len() * n) as f64; + println!("diff: samples={n} per difference, {} differences, wall={:.1}s band=2^-{:.1}", diffs.len(), t0.elapsed().as_secs_f64(), (n as f64).log2()); + println!("max multiplicity of one output difference: {max_mult} (input difference #{arg}, p about {:.2e}); a random map gives 1 or 2", max_mult as f64 / n as f64); + println!("deterministic output bits: total {det_total} over all differences, max per difference {det_max} of 512"); + println!("zero output words: max in any pair {zero_max} of 16, mean per pair {zero_mean:.6} (random: {:.6})", 16.0 / 4294967296.0); + let verdict = if max_mult >= 4 || det_total > 0 || zero_max >= 2 { "DISTINGUISHED" } else { "no differential above the band" }; + println!("verdict: {verdict}"); +} + +// ------------------------------------------------------------------------------------------------------------------ +// lin, lin0: single-bit linear correlations, bitsliced over 64 samples +// ------------------------------------------------------------------------------------------------------------------ + +fn transpose(block: &[[u32; 16]; 64], cols: &mut [u64; 512]) { + for c in cols.iter_mut() { + *c = 0; + } + for (s, st) in block.iter().enumerate() { + for w in 0..16 { + let mut v = st[w]; + while v != 0 { + let b = v.trailing_zeros() as usize; + cols[w * 32 + b] |= 1u64 << s; + v &= v - 1; + } + } + } +} + +fn lin_core(a: &Args, round0: bool) { + let day = a.u64("day", 20729); + let plant = Plant::parse(&a.str("plant", "none")); + let k = a.usize("apps", 8); + let start = a.usize("start", if round0 { 0 } else { 8 }); + let threads = a.usize("threads", 32); + let n = a.u64("samples", 1 << 20); + let blocks = n / 64; + let mp = params_of_day(day, plant); + header(a, &mp, plant, start, k); + let ks = keys(start, k, plant); + let t0 = Instant::now(); + let nin = if round0 { 32 } else { 512 }; + let per = blocks / threads as u64; + let parts: Vec<(Vec, Vec, Vec)> = std::thread::scope(|sc| { + let hs: Vec<_> = (0..threads) + .map(|th| { + let mp = ∓ + let ks = &ks; + sc.spawn(move || { + let mut joint = vec![0u32; nin * 512]; + let mut cx = vec![0u32; nin]; + let mut cy = vec![0u32; 512]; + let mut rng = seed_thread(day, 4, th as u64); + let cnt = if th + 1 == threads { blocks - per * (threads as u64 - 1) } else { per }; + let mut xs = [[0u32; 16]; 64]; + let mut ys = [[0u32; 16]; 64]; + let mut xc = [0u64; 512]; + let mut yc = [0u64; 512]; + for _ in 0..cnt { + for s in 0..64 { + let x = if round0 { + let t = rng.next() as u32; + let mut st = [0u32; 16]; + st[0] = t; + st + } else { + random_state(&mut rng) + }; + xs[s] = x; + let mut y = if round0 { init(x[0], mp) } else { x }; + apply(&mut y, ks, mp, plant); + ys[s] = y; + } + transpose(&xs, &mut xc); + transpose(&ys, &mut yc); + for i in 0..nin { + let xi = xc[i]; + cx[i] += xi.count_ones(); + if xi == 0 { + continue; + } + let row = &mut joint[i * 512..(i + 1) * 512]; + for j in 0..512 { + row[j] += (xi & yc[j]).count_ones(); + } + } + for j in 0..512 { + cy[j] += yc[j].count_ones(); + } + } + (joint, cx, cy) + }) + }) + .collect(); + hs.into_iter().map(|h| h.join().unwrap()).collect() + }); + let mut joint = vec![0u64; nin * 512]; + let mut cx = vec![0u64; nin]; + let mut cy = vec![0u64; 512]; + for (j, x, y) in &parts { + for i in 0..joint.len() { + joint[i] += j[i] as u64; + } + for i in 0..nin { + cx[i] += x[i] as u64; + } + for i in 0..512 { + cy[i] += y[i] as u64; + } + } + let nn = blocks * 64; + let nf = nn as f64; + let sq = nf.sqrt(); + let mut worst = (0usize, 0usize, 0f64); + let mut beyond6 = 0usize; + let mut beyond8 = 0usize; + let mut idx_worst = 0f64; + let mut idx_beyond6 = 0usize; + for i in 0..nin { + for j in 0..512 { + // Pr[x_i = y_j] = (n - cx - cy + 2 joint) / n ; c = 2 Pr - 1 + let eq = nf - cx[i] as f64 - cy[j] as f64 + 2.0 * joint[i * 512 + j] as f64; + let c = 2.0 * eq / nf - 1.0; + let z = c * sq; + if z.abs() > worst.2.abs() { + worst = (i, j, z); + } + if z.abs() > 6.0 { + beyond6 += 1; + } + if z.abs() > 8.0 { + beyond8 += 1; + } + if j < 22 { + if z.abs() > idx_worst.abs() { + idx_worst = z; + } + if z.abs() > 6.0 { + idx_beyond6 += 1; + } + } + } + } + let worst_c = worst.2 / sq; + // output bit balance + let ybal = cy.iter().map(|&c| ((2.0 * c as f64 - nf) / sq).abs()).fold(0f64, f64::max); + println!("{}: samples={nn} wall={:.1}s band(6 sigma)={:.5} inputs={nin}", if round0 { "lin0" } else { "lin" }, t0.elapsed().as_secs_f64(), 6.0 / sq); + println!("worst correlation in={} out={} (word {}) c={worst_c:.5} z={:.2}; cells beyond6={beyond6} beyond8={beyond8} of {}", worst.0, worst.1, worst.1 / 32, worst.2, nin * 512); + println!("line index cells (out bits 0..21): worst z={idx_worst:.2} beyond6={idx_beyond6}; output bit balance worst z={ybal:.2}"); + let verdict = if beyond6 > 0 || ybal > 6.0 { "DISTINGUISHED" } else { "no correlation beyond 6 sigma" }; + println!("verdict: {verdict}"); +} + +// ------------------------------------------------------------------------------------------------------------------ +// rx: rotational-XOR +// ------------------------------------------------------------------------------------------------------------------ + +fn cmd_rx(a: &Args) { + let day = a.u64("day", 20729); + let plant = Plant::parse(&a.str("plant", "none")); + let k = a.usize("apps", 8); + let start = a.usize("start", 8); + let n = a.usize("samples", 1 << 20); + let mp = params_of_day(day, plant); + header(a, &mp, plant, start, k); + let ks = keys(start, k, plant); + let t0 = Instant::now(); + let mut rng = seed_thread(day, 5, 0); + let mut any = false; + for &r in &[1u32, 8, 16] { + let mut ds: Vec> = (0..16).map(|_| Vec::with_capacity(n)).collect(); + let mut zero = [0usize; 16]; + for _ in 0..n { + let x = random_state(&mut rng); + let mut y = x; + apply(&mut y, &ks, &mp, plant); + let mut xr = x; + for w in 0..16 { + xr[w] = xr[w].rotate_left(r); + } + apply(&mut xr, &ks, &mp, plant); + for w in 0..16 { + let d = xr[w] ^ y[w].rotate_left(r); + if d == 0 { + zero[w] += 1; + } + ds[w].push(d); + } + } + let mut best = (0usize, 0u32, 1usize); + for w in 0..16 { + ds[w].sort_unstable(); + let mut run = 1; + for i in 1..n { + if ds[w][i] == ds[w][i - 1] { + run += 1; + if run > best.2 { + best = (w, ds[w][i], run); + } + } else { + run = 1; + } + } + } + let zmax = zero.iter().max().unwrap(); + println!("rx r={r}: zero-difference words max {zmax} of {n} (random {:.2e} expected); most frequent per-word difference word {} value {:#010x} multiplicity {} (random gives about {})", n as f64 / 4294967296.0, best.0, best.1, best.2, 1 + (n as f64 / 4294967296.0).ceil() as usize); + if *zmax > 2 || best.2 > 3 { + any = true; + } + } + println!("rx wall={:.1}s", t0.elapsed().as_secs_f64()); + println!("verdict: {}", if any { "RX PROPERTY" } else { "no rotational-XOR property above the band" }); +} + +// ------------------------------------------------------------------------------------------------------------------ +// cnf: the Tseitin model of k applications on the round-0 input +// ------------------------------------------------------------------------------------------------------------------ + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Lit { + T, + F, + V(i32), // positive or negative DIMACS literal +} + +struct Cnf { + nvars: i32, + clauses: Vec>, +} + +impl Cnf { + fn new() -> Cnf { + Cnf { nvars: 0, clauses: Vec::new() } + } + fn var(&mut self) -> Lit { + self.nvars += 1; + Lit::V(self.nvars) + } + fn not(l: Lit) -> Lit { + match l { + Lit::T => Lit::F, + Lit::F => Lit::T, + Lit::V(v) => Lit::V(-v), + } + } + fn lit(l: Lit) -> i32 { + match l { + Lit::V(v) => v, + _ => panic!("constant literal in a clause"), + } + } + fn xor(&mut self, a: Lit, b: Lit) -> Lit { + match (a, b) { + (Lit::F, x) | (x, Lit::F) => x, + (Lit::T, x) | (x, Lit::T) => Cnf::not(x), + (Lit::V(p), Lit::V(q)) => { + if p == q { + return Lit::F; + } + if p == -q { + return Lit::T; + } + let c = self.var(); + let cc = Cnf::lit(c); + self.clauses.push(vec![-p, -q, -cc]); + self.clauses.push(vec![p, q, -cc]); + self.clauses.push(vec![p, -q, cc]); + self.clauses.push(vec![-p, q, cc]); + c + } + } + } + fn and(&mut self, a: Lit, b: Lit) -> Lit { + match (a, b) { + (Lit::F, _) | (_, Lit::F) => Lit::F, + (Lit::T, x) | (x, Lit::T) => x, + (Lit::V(p), Lit::V(q)) => { + if p == q { + return a; + } + if p == -q { + return Lit::F; + } + let c = self.var(); + let cc = Cnf::lit(c); + self.clauses.push(vec![-cc, p]); + self.clauses.push(vec![-cc, q]); + self.clauses.push(vec![cc, -p, -q]); + c + } + } + } + fn or(&mut self, a: Lit, b: Lit) -> Lit { + let x = self.and(Cnf::not(a), Cnf::not(b)); + Cnf::not(x) + } + /// majority(a, b, c) as the full adder's carry + fn maj(&mut self, a: Lit, b: Lit, c: Lit) -> Lit { + let ab = self.and(a, b); + let ac = self.and(a, c); + let bc = self.and(b, c); + let t = self.or(ab, ac); + self.or(t, bc) + } + fn add32(&mut self, a: &[Lit; 32], b: &[Lit; 32]) -> [Lit; 32] { + let mut out = [Lit::F; 32]; + let mut carry = Lit::F; + for i in 0..32 { + let s1 = self.xor(a[i], b[i]); + out[i] = self.xor(s1, carry); + if i + 1 < 32 { + carry = self.maj(a[i], b[i], carry); + } + } + out + } + fn xor32(&mut self, a: &[Lit; 32], b: &[Lit; 32]) -> [Lit; 32] { + let mut out = [Lit::F; 32]; + for i in 0..32 { + out[i] = self.xor(a[i], b[i]); + } + out + } + fn const32(v: u32) -> [Lit; 32] { + let mut out = [Lit::F; 32]; + for i in 0..32 { + out[i] = if (v >> i) & 1 == 1 { Lit::T } else { Lit::F }; + } + out + } + fn rotl32(a: &[Lit; 32], r: u32) -> [Lit; 32] { + let mut out = [Lit::F; 32]; + for i in 0..32 { + out[((i as u32 + r) % 32) as usize] = a[i]; + } + out + } + fn shl32(a: &[Lit; 32], s: usize) -> [Lit; 32] { + let mut out = [Lit::F; 32]; + for i in s..32 { + out[i] = a[i - s]; + } + out + } + /// a * m mod 2^32 for a constant m, as the binary shift-add chain over the set bits of m. + fn mulc32(&mut self, a: &[Lit; 32], m: u32) -> [Lit; 32] { + let mut acc = Cnf::const32(0); + for i in 0..32 { + if (m >> i) & 1 == 1 { + let sh = Cnf::shl32(a, i); + acc = self.add32(&acc, &sh); + } + } + acc + } + fn qr(&mut self, s: &mut [[Lit; 32]; 16], a: usize, b: usize, c: usize, d: usize, r: [u32; 4]) { + s[a] = self.add32(&s[a], &s[b]); + s[d] = self.xor32(&s[d], &s[a]); + s[d] = Cnf::rotl32(&s[d], r[0]); + s[c] = self.add32(&s[c], &s[d]); + s[b] = self.xor32(&s[b], &s[c]); + s[b] = Cnf::rotl32(&s[b], r[1]); + s[a] = self.add32(&s[a], &s[b]); + s[d] = self.xor32(&s[d], &s[a]); + s[d] = Cnf::rotl32(&s[d], r[2]); + s[c] = self.add32(&s[c], &s[d]); + s[b] = self.xor32(&s[b], &s[c]); + s[b] = Cnf::rotl32(&s[b], r[3]); + } + fn mixer(&mut self, s: &mut [[Lit; 32]; 16], rk: u32, mp: &MixParams) { + for i in 0..16 { + let c = Cnf::const32(mp.rc[i].wrapping_add(rk)); + let x = self.xor32(&s[i], &c); + s[i] = self.mulc32(&x, mp.mul[i]); + } + let r = mp.rot; + let c = [r[0], r[1], r[2], r[3]]; + let d = [r[4], r[5], r[6], r[7]]; + self.qr(s, 0, 4, 8, 12, c); + self.qr(s, 1, 5, 9, 13, c); + self.qr(s, 2, 6, 10, 14, c); + self.qr(s, 3, 7, 11, 15, c); + self.qr(s, 0, 5, 10, 15, d); + self.qr(s, 1, 6, 11, 12, d); + self.qr(s, 2, 7, 8, 13, d); + self.qr(s, 3, 4, 9, 14, d); + } + fn assert_eq_const(&mut self, a: &[Lit], v: u32) { + for (i, &l) in a.iter().enumerate() { + let want = (v >> i) & 1 == 1; + match l { + Lit::T => assert!(want, "constant bit {i} contradicts the target: unsatisfiable by construction"), + Lit::F => assert!(!want, "constant bit {i} contradicts the target: unsatisfiable by construction"), + Lit::V(p) => self.clauses.push(vec![if want { p } else { -p }]), + } + } + } +} + +fn cmd_cnf(a: &Args) { + let day = a.u64("day", 20729); + let plant = Plant::parse(&a.str("plant", "none")); + let k = a.usize("apps", 1); + let start = a.usize("start", 0); + let t0v = a.u64("t0", 0x12345678) as u32; + let out = a.str("out", "mixer.cnf"); + let bits = a.usize("bits", 22); + let mp = params_of_day(day, plant); + header(a, &mp, plant, start, k); + let ks = keys(start, k, plant); + // the target: the index of t0 through the real code + let mut s = init(t0v, &mp); + apply(&mut s, &ks, &mp, plant); + let target = s[0] & ((1u32 << bits) - 1); + println!("t0={t0v:#010x} target index (low {bits} bits of s[0]) = {target:#x}"); + let mut cnf = Cnf::new(); + let mut t = [Lit::F; 32]; + for i in 0..32 { + t[i] = cnf.var(); + } + let mut st = [[Lit::F; 32]; 16]; + for i in 0..8 { + st[i] = Cnf::const32(mp.key[i]); + } + for i in 0..8 { + let m = cnf.mulc32(&t, mp.mul[i]); + st[8 + i] = cnf.add32(&m, &Cnf::const32(mp.rc[i])); + } + let ks_eff: Vec = if plant == Plant::One { ks[..1].to_vec() } else { ks.clone() }; + for &rk in &ks_eff { + cnf.mixer(&mut st, rk, &mp); + } + cnf.assert_eq_const(&st[0][..bits], target); + let mut f = std::io::BufWriter::new(std::fs::File::create(&out).unwrap()); + writeln!(f, "c adv-mixer-3 cnf day={day} apps={} start={start} plant={plant:?} t0={t0v:#010x} target={target:#x} bits={bits}", ks_eff.len()).unwrap(); + writeln!(f, "c t bits are variables 1..32 (bit i = variable i+1)").unwrap(); + writeln!(f, "p cnf {} {}", cnf.nvars, cnf.clauses.len()).unwrap(); + for c in &cnf.clauses { + for l in c { + write!(f, "{l} ").unwrap(); + } + writeln!(f, "0").unwrap(); + } + f.flush().unwrap(); + println!("cnf: vars={} clauses={} written to {out}", cnf.nvars, cnf.clauses.len()); + println!("verify a model with: adv-mixer-3 verify --day {day} --apps {} --start {start} --plant {} --t ", ks_eff.len(), a.str("plant", "none")); +} + +fn cmd_verify(a: &Args) { + let day = a.u64("day", 20729); + let plant = Plant::parse(&a.str("plant", "none")); + let k = a.usize("apps", 1); + let start = a.usize("start", 0); + let bits = a.usize("bits", 22); + let t = a.u64("t", 0) as u32; + let mp = params_of_day(day, plant); + let ks = keys(start, k, plant); + let mut s = init(t, &mp); + apply(&mut s, &ks, &mp, plant); + println!("verify day={day} apps={k} start={start} plant={plant:?} t={t:#010x} s[0]={:#010x} index(low {bits})={:#x}", s[0], s[0] & ((1u32 << bits) - 1)); +} + +/// Read a DIMACS model line ("v 1 -2 3 ... 0", one or several lines) and print t. +fn cmd_model(a: &Args) { + let path = a.str("file", ""); + let txt = std::fs::read_to_string(&path).unwrap(); + let mut t = 0u32; + for line in txt.lines() { + if let Some(rest) = line.strip_prefix("v ") { + for tok in rest.split_whitespace() { + let v: i64 = tok.parse().unwrap_or(0); + if (1..=32).contains(&v) { + t |= 1 << (v - 1); + } + } + } + } + println!("model t={t:#010x} ({t})"); +} + +fn cmd_params(a: &Args) { + let day = a.u64("day", 20729); + let plant = Plant::parse(&a.str("plant", "none")); + let mp = params_of_day(day, plant); + header(a, &mp, plant, 0, APPS_PER_ITEM); +} + +fn main() { + let a = Args::parse(); + match a.cmd.as_str() { + "index" => cmd_index(&a), + "sac" => cmd_sac(&a), + "diff" => cmd_diff(&a), + "lin" => lin_core(&a, false), + "lin0" => lin_core(&a, true), + "rx" => cmd_rx(&a), + "cnf" => cmd_cnf(&a), + "verify" => cmd_verify(&a), + "model" => cmd_model(&a), + "params" => cmd_params(&a), + _ => { + eprintln!("commands: index sac diff lin lin0 rx cnf verify model params (see the header of src/main.rs)"); + std::process::exit(2); + } + } +}