diff --git a/tools/attack/adv-mixer-2/Cargo.lock b/tools/attack/adv-mixer-2/Cargo.lock new file mode 100644 index 000000000..564e168f6 --- /dev/null +++ b/tools/attack/adv-mixer-2/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "attack-adv-mixer-2" +version = "0.1.0" +dependencies = [ + "igneum-pow", +] + +[[package]] +name = "igneum-pow" +version = "0.2.0" diff --git a/tools/attack/adv-mixer-2/Cargo.toml b/tools/attack/adv-mixer-2/Cargo.toml new file mode 100644 index 000000000..6dd671740 --- /dev/null +++ b/tools/attack/adv-mixer-2/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "attack-adv-mixer-2" +version = "0.1.0" +edition = "2021" +description = "Adversarial lane adv-mixer-2: the day-key weakness class of the mixer M_r (weak parameter draws), censused through the real draw code of igneum-pow" +license = "MIT" +publish = false + +[[bin]] +name = "adv-mixer-2" +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-2/queue/01-adv-mixer-2-census-2p24.sh b/tools/attack/adv-mixer-2/queue/01-adv-mixer-2-census-2p24.sh new file mode 100755 index 000000000..85d1ad8dc --- /dev/null +++ b/tools/attack/adv-mixer-2/queue/01-adv-mixer-2-census-2p24.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# adv-mixer-2 queue file (owner: lane adv-mixer-2). Self-contained: binary, args, log, pid file. Runs on either box through /srv/builds/_bin/lease pool (rule of 20:22 BST, 7 October 2026). +set -uo pipefail +B=/srv/builds/_adv-adv-mixer-2/bin/adv-mixer-2 +L=/srv/builds/_adv-adv-mixer-2/logs +mkdir -p "$L" +run() { # : one sweep through lease pool (class adv), log + pid file beside it + local name="$1" thr="$2" mn="$3"; shift 3 + echo "$(date -u +%FT%TZ) start $name (lease pool $thr min $mn): $B $*" >> "$L/$name.log" + /srv/builds/_bin/lease pool "$thr" --min "$mn" --label "adv-mixer-2 $name" --owner adv-mixer-2 -- "$B" "$@" >> "$L/$name.log" 2>&1 & + echo $! > "$L/$name.pid" + wait $! + local rc=$? + echo "$(date -u +%FT%TZ) end $name rc=$rc" >> "$L/$name.log" + rm -f "$L/$name.pid" +} +run census-2p24 32 16 census --from 20729 --count 2^24 --threads {cores} --lowest 16384 --out-days "$L/tail-2p24.txt" diff --git a/tools/attack/adv-mixer-2/queue/02-adv-mixer-2-calendar.sh b/tools/attack/adv-mixer-2/queue/02-adv-mixer-2-calendar.sh new file mode 100755 index 000000000..e5133c3fb --- /dev/null +++ b/tools/attack/adv-mixer-2/queue/02-adv-mixer-2-calendar.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# adv-mixer-2 queue file (owner: lane adv-mixer-2). Self-contained: binary, args, log, pid file. Runs on either box through /srv/builds/_bin/lease pool (rule of 20:22 BST, 7 October 2026). +set -uo pipefail +B=/srv/builds/_adv-adv-mixer-2/bin/adv-mixer-2 +L=/srv/builds/_adv-adv-mixer-2/logs +mkdir -p "$L" +run() { # : one sweep through lease pool (class adv), log + pid file beside it + local name="$1" thr="$2" mn="$3"; shift 3 + echo "$(date -u +%FT%TZ) start $name (lease pool $thr min $mn): $B $*" >> "$L/$name.log" + /srv/builds/_bin/lease pool "$thr" --min "$mn" --label "adv-mixer-2 $name" --owner adv-mixer-2 -- "$B" "$@" >> "$L/$name.log" 2>&1 & + echo $! > "$L/$name.pid" + wait $! + local rc=$? + echo "$(date -u +%FT%TZ) end $name rc=$rc" >> "$L/$name.log" + rm -f "$L/$name.pid" +} +run calendar-100y 32 16 calendar --from 20729 --years 100 --threads {cores} diff --git a/tools/attack/adv-mixer-2/queue/03-adv-mixer-2-scm-calendar.sh b/tools/attack/adv-mixer-2/queue/03-adv-mixer-2-scm-calendar.sh new file mode 100755 index 000000000..19cc06e90 --- /dev/null +++ b/tools/attack/adv-mixer-2/queue/03-adv-mixer-2-scm-calendar.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# adv-mixer-2 queue file (owner: lane adv-mixer-2). Self-contained: binary, args, log, pid file. Runs on either box through /srv/builds/_bin/lease pool (rule of 20:22 BST, 7 October 2026). +set -uo pipefail +B=/srv/builds/_adv-adv-mixer-2/bin/adv-mixer-2 +L=/srv/builds/_adv-adv-mixer-2/logs +mkdir -p "$L" +run() { # : one sweep through lease pool (class adv), log + pid file beside it + local name="$1" thr="$2" mn="$3"; shift 3 + echo "$(date -u +%FT%TZ) start $name (lease pool $thr min $mn): $B $*" >> "$L/$name.log" + /srv/builds/_bin/lease pool "$thr" --min "$mn" --label "adv-mixer-2 $name" --owner adv-mixer-2 -- "$B" "$@" >> "$L/$name.log" 2>&1 & + echo $! > "$L/$name.pid" + wait $! + local rc=$? + echo "$(date -u +%FT%TZ) end $name rc=$rc" >> "$L/$name.log" + rm -f "$L/$name.pid" +} +run scm-calendar 32 16 scm-refine --from 20729 --count 36525 --threads {cores} diff --git a/tools/attack/adv-mixer-2/queue/04-adv-mixer-2-scm-tail-sample.sh b/tools/attack/adv-mixer-2/queue/04-adv-mixer-2-scm-tail-sample.sh new file mode 100755 index 000000000..9cda17f17 --- /dev/null +++ b/tools/attack/adv-mixer-2/queue/04-adv-mixer-2-scm-tail-sample.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# adv-mixer-2 queue file (owner: lane adv-mixer-2). Self-contained: binary, args, log, pid file. Runs on either box through /srv/builds/_bin/lease pool (rule of 20:22 BST, 7 October 2026). +set -uo pipefail +B=/srv/builds/_adv-adv-mixer-2/bin/adv-mixer-2 +L=/srv/builds/_adv-adv-mixer-2/logs +mkdir -p "$L" +run() { # : one sweep through lease pool (class adv), log + pid file beside it + local name="$1" thr="$2" mn="$3"; shift 3 + echo "$(date -u +%FT%TZ) start $name (lease pool $thr min $mn): $B $*" >> "$L/$name.log" + /srv/builds/_bin/lease pool "$thr" --min "$mn" --label "adv-mixer-2 $name" --owner adv-mixer-2 -- "$B" "$@" >> "$L/$name.log" 2>&1 & + echo $! > "$L/$name.pid" + wait $! + local rc=$? + echo "$(date -u +%FT%TZ) end $name rc=$rc" >> "$L/$name.log" + rm -f "$L/$name.pid" +} +while [ ! -s "$L/tail-2p24.txt" ]; do sleep 30; done +run scm-tail-2p24 32 16 scm-refine --days "$L/tail-2p24.txt" --threads {cores} +run scm-sample-2p16 32 16 scm-refine --sample 65536 --seed 1 --threads {cores} diff --git a/tools/attack/adv-mixer-2/queue/05-adv-mixer-2-avalanche.sh b/tools/attack/adv-mixer-2/queue/05-adv-mixer-2-avalanche.sh new file mode 100755 index 000000000..cb804a735 --- /dev/null +++ b/tools/attack/adv-mixer-2/queue/05-adv-mixer-2-avalanche.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# adv-mixer-2 queue file (owner: lane adv-mixer-2). Self-contained: binary, args, log, pid file. Runs on either box through /srv/builds/_bin/lease pool (rule of 20:22 BST, 7 October 2026). +set -uo pipefail +B=/srv/builds/_adv-adv-mixer-2/bin/adv-mixer-2 +L=/srv/builds/_adv-adv-mixer-2/logs +mkdir -p "$L" +run() { # : one sweep through lease pool (class adv), log + pid file beside it + local name="$1" thr="$2" mn="$3"; shift 3 + echo "$(date -u +%FT%TZ) start $name (lease pool $thr min $mn): $B $*" >> "$L/$name.log" + /srv/builds/_bin/lease pool "$thr" --min "$mn" --label "adv-mixer-2 $name" --owner adv-mixer-2 -- "$B" "$@" >> "$L/$name.log" 2>&1 & + echo $! > "$L/$name.pid" + wait $! + local rc=$? + echo "$(date -u +%FT%TZ) end $name rc=$rc" >> "$L/$name.log" + rm -f "$L/$name.pid" +} +run avalanche-genesis 1 1 avalanche --index 20729 --states 1024 +run avalanche-plant-alleq 1 1 avalanche --plant alleq --states 1024 +run avalanche-plant-rot1 1 1 avalanche --plant rot1 --states 1024 +run avalanche-plant-weakday 1 1 avalanche --plant weakday --states 1024 +# the worst ROT days of the 2^24 census, read from its log once it exists +while [ ! -f "$L/census-2p24.log" ] || ! grep -q "end census-2p24" "$L/census-2p24.log"; do sleep 30; done +for cls in "ROT distinct <= 2" "ROT distinct <= 3" "ROT two same-word pairs sum to 32" "ROT >= 4 in {1,31}" "ROT all 8 in {1,2,30,31}" "ROT >= 4 in {8,16,24}" "ROT column set == diagonal set"; do + d=$(grep -F "| $cls |" "$L/census-2p24.log" | head -1 | sed -n 's/.*day \([0-9]*\):.*/\1/p') + [ -n "$d" ] && run "avalanche-day-$d" 1 1 avalanche --index "$d" --states 1024 +done diff --git a/tools/attack/adv-mixer-2/queue/06-adv-mixer-2-census-2p32.sh b/tools/attack/adv-mixer-2/queue/06-adv-mixer-2-census-2p32.sh new file mode 100755 index 000000000..fbe4acb26 --- /dev/null +++ b/tools/attack/adv-mixer-2/queue/06-adv-mixer-2-census-2p32.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# adv-mixer-2 queue file (owner: lane adv-mixer-2). Self-contained: binary, args, log, pid file. Runs on either box through /srv/builds/_bin/lease pool (rule of 20:22 BST, 7 October 2026). +set -uo pipefail +B=/srv/builds/_adv-adv-mixer-2/bin/adv-mixer-2 +L=/srv/builds/_adv-adv-mixer-2/logs +mkdir -p "$L" +run() { # : one sweep through lease pool (class adv), log + pid file beside it + local name="$1" thr="$2" mn="$3"; shift 3 + echo "$(date -u +%FT%TZ) start $name (lease pool $thr min $mn): $B $*" >> "$L/$name.log" + /srv/builds/_bin/lease pool "$thr" --min "$mn" --label "adv-mixer-2 $name" --owner adv-mixer-2 -- "$B" "$@" >> "$L/$name.log" 2>&1 & + echo $! > "$L/$name.pid" + wait $! + local rc=$? + echo "$(date -u +%FT%TZ) end $name rc=$rc" >> "$L/$name.log" + rm -f "$L/$name.pid" +} +run census-2p32 32 16 census --from 20729 --count 2^32 --threads {cores} --lowest 4096 --out-days "$L/tail-2p32.txt" diff --git a/tools/attack/adv-mixer-2/queue/07-adv-mixer-2-redraw.sh b/tools/attack/adv-mixer-2/queue/07-adv-mixer-2-redraw.sh new file mode 100755 index 000000000..ccc449617 --- /dev/null +++ b/tools/attack/adv-mixer-2/queue/07-adv-mixer-2-redraw.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# adv-mixer-2 queue file (owner: lane adv-mixer-2). Self-contained: binary, args, log, pid file. Runs on either box through /srv/builds/_bin/lease pool (rule of 20:22 BST, 7 October 2026). +set -uo pipefail +B=/srv/builds/_adv-adv-mixer-2/bin/adv-mixer-2 +L=/srv/builds/_adv-adv-mixer-2/logs +mkdir -p "$L" +run() { # : one sweep through lease pool (class adv), log + pid file beside it + local name="$1" thr="$2" mn="$3"; shift 3 + echo "$(date -u +%FT%TZ) start $name (lease pool $thr min $mn): $B $*" >> "$L/$name.log" + /srv/builds/_bin/lease pool "$thr" --min "$mn" --label "adv-mixer-2 $name" --owner adv-mixer-2 -- "$B" "$@" >> "$L/$name.log" 2>&1 & + echo $! > "$L/$name.pid" + wait $! + local rc=$? + echo "$(date -u +%FT%TZ) end $name rc=$rc" >> "$L/$name.log" + rm -f "$L/$name.pid" +} +run redraw-2p24-206 32 16 redraw-census --from 20729 --count 2^24 --threads {cores} --max-cost 206 +run redraw-2p28-206 32 16 redraw-census --from 20729 --count 2^28 --threads {cores} --max-cost 206 diff --git a/tools/attack/adv-mixer-2/run-box.sh b/tools/attack/adv-mixer-2/run-box.sh new file mode 100755 index 000000000..c55f14974 --- /dev/null +++ b/tools/attack/adv-mixer-2/run-box.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# adv-mixer-2 chain runner (Mac side). For each queue file named: claim it on build-2 (mkdir +# /srv/builds/_adv/mixer/claims/, atomic; exists = taken), copy its text to the named box and run it there in one +# detached chain, in order. Every queue file is self-contained and starts its binary only through +# `/srv/builds/_bin/lease pool` (class adv, 32 cores, --min 16; single-threaded sweeps take 1), which pins and nices it. +# Logs and pid files live under /srv/builds/_adv-adv-mixer-2/ on the box, never under the worktree mirror. Kill by pid +# file only. History: 19:4x BST the SIGSTOP yield was dropped (cores 8 to 95 at nice 10); 20:22 BST the lease pool +# became the only way to start a sweep; 20:40 BST priority classes (release > v5 > measure > adv). +# Usage: run-box.sh <1|2> ... +set -euo pipefail +BOX="$1"; shift +KEY=~/.ssh/igneum_ed25519 +H1=$(head -1 ~/.config/igneum/build-server); H2=$(head -1 ~/.config/igneum/build-server-2) +case "$BOX" in 1) H="$H1" ;; 2) H="$H2" ;; *) echo "box 1 or 2" >&2; exit 2 ;; esac +QDIR=/srv/builds/_adv/mixer/queue; CDIR=/srv/builds/_adv/mixer/claims +CHAIN="" +for Q in "$@"; do + if ssh -i "$KEY" -o BatchMode=yes "$H2" "mkdir '$CDIR/$Q' 2>/dev/null && echo 'adv-mixer-2 box $BOX '\$(date -u +%FT%TZ) > '$CDIR/$Q/owner'"; then + BODY=$(ssh -i "$KEY" -o BatchMode=yes "$H2" "cat '$QDIR/$Q'") + ssh -i "$KEY" -o BatchMode=yes "$H" "mkdir -p /srv/builds/_adv-adv-mixer-2/logs /srv/builds/_adv-adv-mixer-2/q && cat > /srv/builds/_adv-adv-mixer-2/q/$Q && chmod +x /srv/builds/_adv-adv-mixer-2/q/$Q" <<< "$BODY" + CHAIN="$CHAIN /srv/builds/_adv-adv-mixer-2/q/$Q;" + echo "claimed $Q for box $BOX" + else + echo "queue file $Q is already claimed, skipped" >&2 + fi +done +[ -n "$CHAIN" ] || exit 0 +STAMP=$(date -u +%Y%m%dT%H%M%SZ) +ssh -i "$KEY" -o BatchMode=yes "$H" "cd /srv/builds/_adv-adv-mixer-2 && setsid nohup bash -c '$CHAIN' > logs/chain-$STAMP.log 2>&1 < /dev/null & echo \$! > /srv/builds/_adv-adv-mixer-2/logs/chain-$STAMP.pid; echo chain started on box $BOX: pid \$(cat /srv/builds/_adv-adv-mixer-2/logs/chain-$STAMP.pid), log logs/chain-$STAMP.log" diff --git a/tools/attack/adv-mixer-2/src/main.rs b/tools/attack/adv-mixer-2/src/main.rs new file mode 100644 index 000000000..70a941ddd --- /dev/null +++ b/tools/attack/adv-mixer-2/src/main.rs @@ -0,0 +1,1370 @@ +//! adv-mixer-2: the day-key weakness class of the mixer M_r (weak parameter draws), censused through the real draw +//! code of `igneum-pow` (`bind::day_bytes`, `seed_words_from_bytes`, `MixParams::with_shape` under +//! `Shape::for_class(&V4_CLASS)`, `memhard::mixer`). Nothing of the hash is re-implemented here. +//! +//! Internal adversarial pass, not an independent review. +//! +//! Cost models (docs/plans/cryptanalysis/plan-mixer-2.md section 2), per mixer application, for a datapath built for +//! the day (constant rotations are wiring, constant XORs are inverters, both 0 on every day): +//! A 64 + sum_i (w32(MUL_i) - 1) w32 = minimal signed-digit weight of MUL_i modulo 2^32 +//! B 64 + sum_i min(w32_i - 1, cert_i) cert_i = certified shift-add chain length (exact sets for 1..4 adders, +//! a 5-adder certificate by decomposition; else none) +//! C 16 / (16 - k) k = words whose constant is in the exact 2-adder set (leaves its DSP block) +//! Gain of a day = median day's cost / the day's cost. +//! +//! Commands: +//! adv-mixer-2 day --index D one day's draw, classes, costs +//! adv-mixer-2 plant alleq|mul1|mul1all|rcrk0|rot1|shift the known-failed shapes through the same classifier +//! adv-mixer-2 census --from D --count N --threads T [--lowest K] [--out-days FILE] +//! adv-mixer-2 exact --threads T w32 table over all 2^31 odd constants, 16-fold convolution +//! adv-mixer-2 scm-build [--out FILE] exact adder-count sets L1..L4 (restricted form), counts +//! adv-mixer-2 scm-refine --days FILE|--from D --count N|--sample N --seed S --threads T model B on named days +//! adv-mixer-2 calendar --from D --years Y the real calendar: worst days with dates, cross-day structure +//! adv-mixer-2 avalanche --index D [--plant P] [--states N] diffusion of 1, 2, 4, 8 applications and the address bits +//! adv-mixer-2 redraw-census --from D --count N --threads T --max-cost X the redraw rule applied, residual tail + +use igneum_pow::bind::day_bytes; +use igneum_pow::generator::V4_CLASS; +use igneum_pow::memhard::{mixer, round_key_mult, MixParams, Shape, ITEM_ROUNDS}; +use igneum_pow::seed::{seed_words_from_bytes, SplitMix64}; +use std::collections::{HashMap, HashSet}; +use std::io::{BufRead, Write}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Instant; + +const GAMMA: u64 = 0x9E3779B97F4A7C15; +const QR_OPS: u32 = 64; +const GENESIS_DAY: u64 = 20_729; +const MIXER_MULT: usize = 8; +const APPS_PER_ITEM: usize = (ITEM_ROUNDS + 1) * MIXER_MULT; + +// ------------------------------------------------------------------------------------------------------------ +// The draw, through the real code +// ------------------------------------------------------------------------------------------------------------ + +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 +} + +fn key_of_day(d: u64) -> [u32; 8] { + seed_words_from_bytes(&day_bytes(d)) +} + +fn params_of_day(d: u64) -> MixParams { + MixParams::with_shape(key_of_day(d), v4_shape()) +} + +fn seed64(key: &[u32; 8]) -> u64 { + key[0] as u64 | ((key[1] as u64) << 32) +} + +fn round_keys() -> [u32; APPS_PER_ITEM] { + let mut k = [0u32; APPS_PER_ITEM]; + for r in 0..=ITEM_ROUNDS { + for j in 0..MIXER_MULT { + k[r * MIXER_MULT + j] = round_key_mult(r, j, MIXER_MULT); + } + } + k +} + +/// Civil date (Howard Hinnant) of a Unix day count. +fn civil(day: u64) -> String { + let z = day as i64 + 719_468; + let era = z.div_euclid(146_097); + let doe = z.rem_euclid(146_097); + let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + let y = if m <= 2 { y + 1 } else { y }; + format!("{y:04}-{m:02}-{d:02}") +} + +// ------------------------------------------------------------------------------------------------------------ +// Signed-digit weight modulo 2^32 +// ------------------------------------------------------------------------------------------------------------ + +/// NAF weight and top digit position of a non-negative integer. +fn naf(mut n: u64) -> (u32, u32) { + let mut w = 0; + let mut pos = 0; + let mut top = 0; + while n != 0 { + if n & 1 == 1 { + if n & 3 == 3 { + n += 1; + } else { + n -= 1; + } + w += 1; + top = pos; + } + n >>= 1; + pos += 1; + } + (w, top) +} + +/// Minimal signed-digit weight of `c` modulo 2^32: digits in positions 0..31 only, the represented integer is `c` or +/// `c - 2^32`. For an integer below 2^32 in magnitude the NAF is minimal among all signed-digit forms, and the +/// candidate whose NAF needs position 32 is discarded (one of the two always fits). +fn w32(c: u32) -> u32 { + if c == 0 { + return 0; + } + let (wa, ta) = naf(c as u64); + let (wb, tb) = naf((1u64 << 32) - c as u64); + let a = if ta <= 31 { wa } else { u32::MAX }; + let b = if tb <= 31 { wb } else { u32::MAX }; + a.min(b) +} + +// ------------------------------------------------------------------------------------------------------------ +// Shift-add chains modulo 2^32 (restricted form): exact level sets +// ------------------------------------------------------------------------------------------------------------ +// +// A value of cost k is `a + (b << s)`, `a - (b << s)`, `(b << s) - a` for odd a of cost i, odd b of cost j, +// i + j = k - 1, s in 1..31; or a product `a * b` with i + j = k. Cost 0 is {1}. Negation closes every level +// (`-c = 1*c - (c << 1)` is one adder, but the three signed forms already give both signs of every value), so cost(c) +// = cost(-c). This is a SUBSET of what a synthesiser can do (no odd-part normalisation across shifts), so every +// level is an upper bound on the attacker's adder count and the gain it yields is a lower bound on his. + +struct Scm { + /// levels[k] = sorted odd values of cost exactly k, k = 0..=3 + levels: Vec>, + /// bitmap over odd values (index c >> 1) of cost <= 4 + le4: Vec, + counts: [u64; 5], +} + +fn bit_set(bm: &[AtomicU64], c: u32) { + let i = (c >> 1) as usize; + bm[i >> 6].fetch_or(1u64 << (i & 63), Ordering::Relaxed); +} +fn bit_get(bm: &[AtomicU64], c: u32) -> bool { + let i = (c >> 1) as usize; + bm[i >> 6].load(Ordering::Relaxed) >> (i & 63) & 1 == 1 +} + +fn combine(a: u32, b: u32, out: &mut impl FnMut(u32)) { + for s in 1..32u32 { + let bs = b.wrapping_shl(s); + out(a.wrapping_add(bs)); + out(a.wrapping_sub(bs)); + out(bs.wrapping_sub(a)); + } +} + +impl Scm { + fn build(threads: usize) -> Scm { + let t0 = Instant::now(); + let mut levels: Vec> = vec![vec![1u32]]; + let mut known: HashSet = HashSet::new(); + known.insert(1); + for k in 1..=3usize { + let mut set: HashSet = HashSet::new(); + let mut push = |v: u32| { + if v & 1 == 1 && !known.contains(&v) { + set.insert(v); + } + }; + for i in 0..k { + let j = k - 1 - i; + for &a in &levels[i] { + for &b in &levels[j] { + combine(a, b, &mut push); + } + } + } + for i in 1..k { + let j = k - i; + if i <= j { + for &a in &levels[i] { + for &b in &levels[j] { + push(a.wrapping_mul(b)); + } + } + } + } + let mut v: Vec = set.into_iter().collect(); + v.sort_unstable(); + known.extend(v.iter().copied()); + eprintln!("scm: level {k}: {} values ({:.1} s)", v.len(), t0.elapsed().as_secs_f64()); + levels.push(v); + } + // level 4 as a bitmap: (L3, L0), (L2, L1), (L1, L2), (L0, L3) combinations and the products L3 x L1, L2 x L2 + let le4: Vec = (0..(1usize << 25)).map(|_| AtomicU64::new(0)).collect(); + for lv in &levels { + for &c in lv { + bit_set(&le4, c); + } + } + let pairs: Vec<(usize, usize)> = vec![(3, 0), (2, 1), (1, 2), (0, 3)]; + std::thread::scope(|sc| { + for t in 0..threads { + let le4 = &le4; + let levels = &levels; + let pairs = &pairs; + sc.spawn(move || { + for &(i, j) in pairs.iter() { + let la = &levels[i]; + for (idx, &a) in la.iter().enumerate() { + if idx % threads != t { + continue; + } + for &b in &levels[j] { + combine(a, b, &mut |v| { + if v & 1 == 1 { + bit_set(le4, v) + } + }); + } + } + } + for (idx, &a) in levels[3].iter().enumerate() { + if idx % threads != t { + continue; + } + for &b in &levels[1] { + bit_set(le4, a.wrapping_mul(b)); + } + } + for (idx, &a) in levels[2].iter().enumerate() { + if idx % threads != t { + continue; + } + for &b in &levels[2] { + bit_set(le4, a.wrapping_mul(b)); + } + } + }); + } + }); + let n4: u64 = le4.iter().map(|w| w.load(Ordering::Relaxed).count_ones() as u64).sum(); + let mut counts = [0u64; 5]; + for k in 0..=3 { + counts[k] = levels[k].len() as u64; + } + counts[4] = n4 - counts[..4].iter().sum::(); + eprintln!("scm: level 4: {} values; cost <= 4: {} of 2^31 odd ({:.4}) ({:.1} s)", counts[4], n4, n4 as f64 / 2f64.powi(31), t0.elapsed().as_secs_f64()); + Scm { levels, le4, counts } + } + + fn in_level_le3(&self, c: u32) -> Option { + for k in 0..=3 { + if self.levels[k].binary_search(&c).is_ok() { + return Some(k as u32); + } + } + None + } + + /// Certified adder count: exact for 0..=4; 5 when a decomposition over the exact sets exists; None otherwise. + fn certify(&self, c: u32) -> Option { + if let Some(k) = self.in_level_le3(c) { + return Some(k); + } + if bit_get(&self.le4, c) { + return Some(4); + } + // 5 adders: c = a +- (1 << s) or (1 << s) - a with a of cost 4: a in {c -+ 2^s, 2^s - c} + for s in 1..32u32 { + let p = 1u32 << s; + for a in [c.wrapping_sub(p), c.wrapping_add(p), p.wrapping_sub(c)] { + if bit_get(&self.le4, a) { + return Some(5); + } + } + } + // c = a * b, a of cost 4, b of cost 1: a = c * b^-1 + for &b in &self.levels[1] { + let inv = inv_mod(b); + if bit_get(&self.le4, c.wrapping_mul(inv)) { + return Some(5); + } + } + // c = a +- (b << s) with a of cost 3 (set), b of cost 1; and a of cost 2, b of cost 2 + for (i, j) in [(3usize, 1usize), (2, 2), (1, 3)] { + for &b in &self.levels[j] { + for s in 1..32u32 { + let bs = b.wrapping_shl(s); + for a in [c.wrapping_sub(bs), c.wrapping_add(bs), bs.wrapping_sub(c)] { + if self.levels[i].binary_search(&a).is_ok() { + return Some(5); + } + } + } + } + } + // c = a * b with a of cost 3, b of cost 2 (and 2, 3) + for (i, j) in [(3usize, 2usize), (2, 3)] { + for &b in &self.levels[j] { + if self.levels[i].binary_search(&c.wrapping_mul(inv_mod(b))).is_ok() { + return Some(5); + } + } + } + None + } +} + +/// Inverse of an odd 32-bit value modulo 2^32 (Newton). +fn inv_mod(b: u32) -> u32 { + let mut x = b; // correct to 3 bits + for _ in 0..5 { + x = x.wrapping_mul(2u32.wrapping_sub(b.wrapping_mul(x))); + } + debug_assert_eq!(b.wrapping_mul(x), 1); + x +} + +// ------------------------------------------------------------------------------------------------------------ +// Classification +// ------------------------------------------------------------------------------------------------------------ + +const CLASSES: &[&str] = &[ + "ROT all equal", + "ROT distinct <= 2", + "ROT distinct <= 3", + "ROT distinct <= 4", + "ROT max multiplicity >= 4", + "ROT same-word pair sums to 32", + "ROT two same-word pairs sum to 32", + "ROT any pair sums to 32", + "ROT >= 4 in {1,31}", + "ROT all 8 in {1,2,30,31}", + "ROT >= 4 in {8,16,24}", + "ROT column set == diagonal set", + "MUL any = 1", + "MUL any = 2^32-1", + "MUL any involution (x^2 = 1)", + "MUL any w32 <= 2", + "MUL any w32 <= 3", + "MUL any w32 <= 4", + "MUL >= 2 with w32 <= 4", + "MUL any popcount <= 4 or >= 28", + "MUL two equal", + "MUL two inverse (a*b = 1)", + "MUL any in exact 2-adder set (model C k >= 1)", + "MUL model C k >= 2", + "RC any = 0", + "RC any popcount <= 4 or >= 28", + "RC + rk = 0 for any of the 72 keys", + "RC two equal", + "RC[i] + rk = RC[j] + rk' for some i != j, two of the 72 keys", + "cost A gain >= 1.1x", + "cost A gain >= 1.2x", + "cost A gain >= 1.5x", +]; + +/// The median cost A every gain is measured against: the exact value from `exact` is 226 (the default), and +/// `--median N` overrides it; the census also reports the full histogram so any other median can be applied after +/// the run. +static MEDIAN_A_CELL: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(226); +#[allow(non_snake_case)] +fn MEDIAN_A() -> u32 { + MEDIAN_A_CELL.load(Ordering::Relaxed) +} + +#[derive(Clone, Debug, Default)] +struct DayClass { + flags: Vec, + cost_a: u32, + k_c: u32, + w: [u32; 16], +} + +struct Classifier { + rks: [u32; APPS_PER_ITEM], + two_adder: Vec, + inv_gamma32: u32, +} + +impl Classifier { + fn new() -> Classifier { + // the exact 2-adder set, built in place (levels 0..=2 are tiny) + let mut levels: Vec> = vec![vec![1u32]]; + let mut known: HashSet = HashSet::new(); + known.insert(1); + for k in 1..=2usize { + let mut set: HashSet = HashSet::new(); + let mut push = |v: u32| { + if v & 1 == 1 && !known.contains(&v) { + set.insert(v); + } + }; + for i in 0..k { + for &a in &levels[i] { + for &b in &levels[k - 1 - i] { + combine(a, b, &mut push); + } + } + } + if k == 2 { + for &a in &levels[1] { + for &b in &levels[1] { + push(a.wrapping_mul(b)); + } + } + } + let mut v: Vec = set.into_iter().collect(); + v.sort_unstable(); + known.extend(v.iter().copied()); + levels.push(v); + } + let mut two_adder: Vec = levels.iter().flatten().copied().collect(); + two_adder.sort_unstable(); + Classifier { rks: round_keys(), two_adder, inv_gamma32: inv_mod(0x9E3779B9) } + } + + fn classify(&self, mp: &MixParams) -> DayClass { + let mut f = vec![false; CLASSES.len()]; + // ROT + let mut seen = [0u32; 32]; + let (mut n1, mut n4, mut nbyte) = (0, 0, 0); + for &r in &mp.rot { + seen[r as usize] += 1; + if r == 1 || r == 31 { + n1 += 1; + } + if matches!(r, 1 | 2 | 30 | 31) { + n4 += 1; + } + if matches!(r, 8 | 16 | 24) { + nbyte += 1; + } + } + let distinct = seen.iter().filter(|&&n| n > 0).count(); + let maxmult = *seen.iter().max().unwrap(); + f[0] = distinct == 1; + f[1] = distinct <= 2; + f[2] = distinct <= 3; + f[3] = distinct <= 4; + f[4] = maxmult >= 4; + let mut sw = 0; + for (a, b) in [(0, 2), (1, 3), (4, 6), (5, 7)] { + if mp.rot[a] + mp.rot[b] == 32 { + sw += 1; + } + } + f[5] = sw >= 1; + f[6] = sw >= 2; + 'outer: for a in 0..8 { + for b in a + 1..8 { + if mp.rot[a] + mp.rot[b] == 32 { + f[7] = true; + break 'outer; + } + } + } + f[8] = n1 >= 4; + f[9] = n4 == 8; + f[10] = nbyte >= 4; + { + let mut c: Vec = mp.rot[..4].to_vec(); + let mut d: Vec = mp.rot[4..].to_vec(); + c.sort_unstable(); + d.sort_unstable(); + f[11] = c == d; + } + // MUL + let mut w = [0u32; 16]; + let (mut sum, mut n_le4, mut k_c) = (0u32, 0, 0u32); + for i in 0..16 { + let m = mp.mul[i]; + w[i] = w32(m); + sum += w[i]; + if m == 1 { + f[12] = true; + } + if m == u32::MAX { + f[13] = true; + } + if m.wrapping_mul(m) == 1 { + f[14] = true; + } + if w[i] <= 2 { + f[15] = true; + } + if w[i] <= 3 { + f[16] = true; + } + if w[i] <= 4 { + f[17] = true; + n_le4 += 1; + } + let p = m.count_ones(); + if p <= 4 || p >= 28 { + f[19] = true; + } + for j in 0..i { + if mp.mul[j] == m { + f[20] = true; + } + if mp.mul[j].wrapping_mul(m) == 1 { + f[21] = true; + } + } + if self.two_adder.binary_search(&m).is_ok() { + k_c += 1; + } + } + f[18] = n_le4 >= 2; + f[22] = k_c >= 1; + f[23] = k_c >= 2; + // RC + for i in 0..16 { + let r = mp.rc[i]; + if r == 0 { + f[24] = true; + } + let p = r.count_ones(); + if p <= 4 || p >= 28 { + f[25] = true; + } + for &rk in self.rks.iter() { + if r.wrapping_add(rk) == 0 { + f[26] = true; + } + } + for j in 0..i { + if mp.rc[j] == r { + f[27] = true; + } + // RC[i] + rk_a = RC[j] + rk_b <=> RC[i] - RC[j] = (b - a) * 0x9E3779B9 with |b - a| <= 71 + let m = r.wrapping_sub(mp.rc[j]).wrapping_mul(self.inv_gamma32) as i32; + if m != 0 && m.unsigned_abs() <= (APPS_PER_ITEM as u32 - 1) { + f[28] = true; + } + } + } + let cost_a = QR_OPS + sum - 16; + f[29] = cost_a * 11 <= MEDIAN_A() * 10; + f[30] = cost_a * 12 <= MEDIAN_A() * 10; + f[31] = cost_a * 15 <= MEDIAN_A() * 10; + DayClass { flags: f, cost_a, k_c, w } + } +} + +// ------------------------------------------------------------------------------------------------------------ +// Tally +// ------------------------------------------------------------------------------------------------------------ + +#[derive(Clone, Copy, Debug)] +struct Worst { + day: u64, + cost: u32, +} + +#[derive(Clone)] +struct Tally { + n: u64, + class_count: Vec, + class_worst: Vec, + cost_hist: Vec, + w_hist: Vec, + kc_hist: [u64; 17], + lowest: Vec<(u32, u64)>, + cap: usize, +} + +impl Tally { + fn new(cap: usize) -> Tally { + Tally { + n: 0, + class_count: vec![0; CLASSES.len()], + class_worst: vec![Worst { day: u64::MAX, cost: u32::MAX }; CLASSES.len()], + cost_hist: vec![0; 512], + w_hist: vec![0; 40], + kc_hist: [0; 17], + lowest: Vec::new(), + cap, + } + } + fn add(&mut self, day: u64, c: &DayClass) { + self.n += 1; + for (i, &on) in c.flags.iter().enumerate() { + if on { + self.class_count[i] += 1; + let wst = &mut self.class_worst[i]; + if c.cost_a < wst.cost || (c.cost_a == wst.cost && day < wst.day) { + *wst = Worst { day, cost: c.cost_a }; + } + } + } + self.cost_hist[c.cost_a as usize] += 1; + for &w in &c.w { + self.w_hist[w as usize] += 1; + } + self.kc_hist[c.k_c as usize] += 1; + if self.lowest.len() < self.cap || c.cost_a < self.lowest.last().map(|x| x.0).unwrap_or(u32::MAX) { + let pos = self.lowest.partition_point(|x| x.0 < c.cost_a || (x.0 == c.cost_a && x.1 < day)); + self.lowest.insert(pos, (c.cost_a, day)); + if self.lowest.len() > self.cap { + self.lowest.pop(); + } + } + } + fn merge(&mut self, o: Tally) { + self.n += o.n; + for i in 0..CLASSES.len() { + self.class_count[i] += o.class_count[i]; + let (a, b) = (self.class_worst[i], o.class_worst[i]); + if b.cost < a.cost || (b.cost == a.cost && b.day < a.day) { + self.class_worst[i] = b; + } + } + for (a, b) in self.cost_hist.iter_mut().zip(o.cost_hist.iter()) { + *a += b; + } + for (a, b) in self.w_hist.iter_mut().zip(o.w_hist.iter()) { + *a += b; + } + for i in 0..17 { + self.kc_hist[i] += o.kc_hist[i]; + } + let cap = self.cap; + let mut all = std::mem::take(&mut self.lowest); + all.extend(o.lowest); + all.sort_unstable(); + all.truncate(cap); + self.lowest = all; + } + fn median(&self) -> u32 { + let half = self.n / 2; + let mut acc = 0u64; + for (c, &h) in self.cost_hist.iter().enumerate() { + acc += h; + if acc > half { + return c as u32; + } + } + 0 + } + fn mean(&self) -> f64 { + let s: f64 = self.cost_hist.iter().enumerate().map(|(c, &h)| c as f64 * h as f64).sum(); + s / self.n as f64 + } + fn count_le(&self, c: u32) -> u64 { + self.cost_hist[..=c as usize].iter().sum() + } + fn print(&self, from: u64, label: &str) { + let n = self.n as f64; + println!("## {label}: {} days from {from}", self.n); + println!(); + println!("| Class | Count | Fraction | log2 | Worst day: cost A, gain A, date |"); + println!("|---|---|---|---|---|"); + for (i, name) in CLASSES.iter().enumerate() { + let c = self.class_count[i]; + let fr = c as f64 / n; + let w = self.class_worst[i]; + let worst = if w.day == u64::MAX { + "none".to_string() + } else { + format!("day {}: {}, {:.4}x, {}", w.day, w.cost, MEDIAN_A() as f64 / w.cost as f64, civil(w.day)) + }; + let l2 = if c == 0 { "-inf".to_string() } else { format!("{:.1}", fr.log2()) }; + println!("| {name} | {c} | {fr:.3e} | {l2} | {worst} |"); + } + println!(); + let med = self.median(); + println!("| Cost A statistic | Value |"); + println!("|---|---|"); + println!("| mean | {:.3} |", self.mean()); + println!("| median (this census) | {med} |"); + println!("| median used for gains | {} |", MEDIAN_A()); + let (lo, hi) = (self.cost_hist.iter().position(|&h| h > 0).unwrap_or(0), self.cost_hist.iter().rposition(|&h| h > 0).unwrap_or(0)); + println!("| min | {lo} (gain {:.4}x) |", MEDIAN_A() as f64 / lo as f64); + println!("| max | {hi} |"); + for g in [1.05f64, 1.1, 1.2, 1.5, 2.0] { + let c = (MEDIAN_A() as f64 / g).floor() as u32; + let k = self.count_le(c); + println!("| days with gain A >= {g}x (cost <= {c}) | {k} ({:.3e}, log2 {}) |", k as f64 / n, if k == 0 { "-inf".to_string() } else { format!("{:.1}", (k as f64 / n).log2()) }); + } + println!(); + println!("| Model C k | Days |"); + println!("|---|---|"); + for k in 0..17 { + if self.kc_hist[k] > 0 { + println!("| {k} (gain {:.4}x) | {} |", 16.0 / (16.0 - k as f64), self.kc_hist[k]); + } + } + println!(); + println!("| w32 per word | Count | Fraction |"); + println!("|---|---|---|"); + let tw: u64 = self.w_hist.iter().sum(); + for (w, &h) in self.w_hist.iter().enumerate() { + if h > 0 { + println!("| {w} | {h} | {:.3e} |", h as f64 / tw as f64); + } + } + println!(); + println!("| Cost A | Days |"); + println!("|---|---|"); + for (c, &h) in self.cost_hist.iter().enumerate() { + if h > 0 { + println!("| {c} | {h} |"); + } + } + println!(); + println!("Lowest-cost days (cost A, day, date, gain A):"); + for &(c, d) in &self.lowest { + println!(" {c} {d} {} {:.4}x", civil(d), MEDIAN_A() as f64 / c as f64); + } + println!(); + } +} + +fn census(from: u64, count: u64, threads: usize, cap: usize, out_days: Option) { + let t0 = Instant::now(); + let cl = Classifier::new(); + let tallies: Vec = std::thread::scope(|sc| { + let hs: Vec<_> = (0..threads) + .map(|t| { + let cl = &cl; + sc.spawn(move || { + let mut tally = Tally::new(cap); + let chunk = count.div_ceil(threads as u64); + let lo = from + t as u64 * chunk; + let hi = (lo + chunk).min(from + count); + for d in lo..hi { + let mp = params_of_day(d); + let c = cl.classify(&mp); + tally.add(d, &c); + } + tally + }) + }) + .collect(); + hs.into_iter().map(|h| h.join().unwrap()).collect() + }); + let mut all = Tally::new(cap); + for t in tallies { + all.merge(t); + } + println!("census: {count} days from {from}, {threads} threads, {:.1} s", t0.elapsed().as_secs_f64()); + all.print(from, "Census"); + if let Some(p) = out_days { + let mut f = std::fs::File::create(&p).unwrap(); + for &(c, d) in &all.lowest { + writeln!(f, "{d} {c}").unwrap(); + } + println!("lowest {} days written to {p}", all.lowest.len()); + } +} + +// ------------------------------------------------------------------------------------------------------------ +// One day, plants +// ------------------------------------------------------------------------------------------------------------ + +fn print_day(label: &str, mp: &MixParams, cl: &Classifier, scm: Option<&Scm>) { + let c = cl.classify(mp); + println!("### {label}"); + println!(); + println!("| Field | Value |"); + println!("|---|---|"); + println!("| seed64 | {:016x} |", seed64(&mp.key)); + println!("| ROT | {} |", mp.rot.iter().map(|r| r.to_string()).collect::>().join(" ")); + println!("| MUL | {} |", mp.mul.iter().map(|m| format!("{m:08x}")).collect::>().join(" ")); + println!("| w32(MUL) | {} |", c.w.iter().map(|w| w.to_string()).collect::>().join(" ")); + if let Some(s) = scm { + let cert: Vec = mp.mul.iter().map(|&m| s.certify(m).map(|k| k.to_string()).unwrap_or("-".into())).collect(); + println!("| certified adders | {} |", cert.join(" ")); + let cost_b: u32 = QR_OPS + mp.mul.iter().zip(c.w.iter()).map(|(&m, &w)| s.certify(m).unwrap_or(u32::MAX).min(w - 1)).sum::(); + println!("| cost B | {cost_b} |"); + } + println!("| RC | {} |", mp.rc.iter().map(|m| format!("{m:08x}")).collect::>().join(" ")); + println!("| cost A | {} (gain {:.4}x against {}) |", c.cost_a, MEDIAN_A() as f64 / c.cost_a as f64, MEDIAN_A()); + println!("| model C k | {} (gain {:.4}x) |", c.k_c, 16.0 / (16.0 - c.k_c as f64)); + let fired: Vec<&str> = CLASSES.iter().zip(c.flags.iter()).filter(|(_, &on)| on).map(|(n, _)| *n).collect(); + println!("| classes fired | {} |", if fired.is_empty() { "none".to_string() } else { fired.join("; ") }); + println!(); +} + +fn plant(kind: &str) -> MixParams { + let mut mp = params_of_day(GENESIS_DAY); + match kind { + "alleq" => mp.rot = [mp.rot[0]; 8], + "rot1" => mp.rot = [1; 8], + "mul1" => mp.mul[5] = 1, + "mul1all" => mp.mul = [1; 16], + "rcrk0" => mp.rc[3] = 0u32.wrapping_sub(round_key_mult(2, 5, MIXER_MULT)), + "weakday" => { + mp.rot = [mp.rot[0]; 8]; + mp.mul = [1; 16]; + } + _ => panic!("unknown plant {kind}"), + } + mp +} + +// ------------------------------------------------------------------------------------------------------------ +// Exact: w32 over every odd 32-bit constant, and the 16-fold convolution +// ------------------------------------------------------------------------------------------------------------ + +fn exact(threads: usize) { + let t0 = Instant::now(); + let hist: Vec<[u64; 40]> = std::thread::scope(|sc| { + let hs: Vec<_> = (0..threads) + .map(|t| { + sc.spawn(move || { + let mut h = [0u64; 40]; + let n = 1u64 << 31; + let chunk = n.div_ceil(threads as u64); + let lo = t as u64 * chunk; + let hi = (lo + chunk).min(n); + for i in lo..hi { + let c = ((i << 1) | 1) as u32; + h[w32(c) as usize] += 1; + } + h + }) + }) + .collect(); + hs.into_iter().map(|h| h.join().unwrap()).collect() + }); + let mut h = [0u64; 40]; + for t in &hist { + for i in 0..40 { + h[i] += t[i]; + } + } + let total: u64 = h.iter().sum(); + assert_eq!(total, 1u64 << 31); + println!("exact: w32 over 2^31 odd constants, {:.1} s", t0.elapsed().as_secs_f64()); + println!(); + println!("| w32 | Constants | Fraction | log2 |"); + println!("|---|---|---|---|"); + let mut mean = 0.0; + for (w, &c) in h.iter().enumerate() { + if c > 0 { + let f = c as f64 / total as f64; + mean += w as f64 * f; + println!("| {w} | {c} | {f:.4e} | {:.2} |", f.log2()); + } + } + println!(); + println!("mean w32 per word {mean:.4}; expected cost A mean {:.4}", QR_OPS as f64 + 16.0 * (mean - 1.0)); + // 16-fold convolution of the (w - 1) distribution, then + 64 + let p: Vec = h.iter().map(|&c| c as f64 / total as f64).collect(); + let mut dist = vec![1.0f64]; + for _ in 0..16 { + let mut nd = vec![0.0f64; dist.len() + 39]; + for (i, &a) in dist.iter().enumerate() { + if a == 0.0 { + continue; + } + for (w, &b) in p.iter().enumerate() { + if w >= 1 && b > 0.0 { + nd[i + w - 1] += a * b; + } + } + } + dist = nd; + } + // cost = 64 + sum + let mut acc = 0.0; + let mut median = 0; + for (s, &pr) in dist.iter().enumerate() { + acc += pr; + if acc >= 0.5 { + median = QR_OPS as usize + s; + break; + } + } + if std::env::args().all(|a| a != "--median") { + MEDIAN_A_CELL.store(median as u32, Ordering::Relaxed); + } + println!("exact cost A median {median}; gains below are against {}", MEDIAN_A()); + println!(); + println!("| Gain A threshold | Cost A <= | P(day) exact | log2 | Days per 100 years (36,525) | Expected in 2^24 | Expected in 2^32 |"); + println!("|---|---|---|---|---|---|---|"); + for g in [1.05f64, 1.1, 1.15, 1.2, 1.3, 1.5, 2.0, 3.0] { + let c = (MEDIAN_A() as f64 / g).floor() as usize; + let pr: f64 = dist.iter().enumerate().filter(|(s, _)| QR_OPS as usize + s <= c).map(|(_, &p)| p).sum(); + let l2 = if pr > 0.0 { format!("{:.1}", pr.log2()) } else { "-inf".into() }; + println!("| {g}x | {c} | {pr:.4e} | {l2} | {:.4} | {:.2} | {:.1} |", pr * 36_525.0, pr * 2f64.powi(24), pr * 2f64.powi(32)); + } + println!(); + println!("| Cost A | P exact | log2 |"); + println!("|---|---|---|"); + for (s, &pr) in dist.iter().enumerate() { + if pr > 1e-30 { + println!("| {} | {pr:.4e} | {:.1} |", QR_OPS as usize + s, pr.log2()); + } + } + // model C exact: P(word in exact 2-adder set) + let cl = Classifier::new(); + let pw = cl.two_adder.len() as f64 / total as f64; + println!(); + println!("| Model C | Value |"); + println!("|---|---|"); + println!("| exact 2-adder set size (cost 0..=2) | {} |", cl.two_adder.len()); + println!("| P(word) | {pw:.4e} (log2 {:.1}) |", pw.log2()); + let p1 = 1.0 - (1.0 - pw).powi(16); + let p2 = 1.0 - (1.0 - pw).powi(16) - 16.0 * pw * (1.0 - pw).powi(15); + println!("| P(k >= 1, gain 1.067x) | {p1:.4e} (log2 {:.1}) |", p1.log2()); + println!("| P(k >= 2, gain 1.143x) | {p2:.4e} (log2 {:.1}) |", p2.log2()); + println!("| P(MUL any = 1) | {:.4e} (log2 {:.1}) |", 16.0 / 2f64.powi(31), (16.0 / 2f64.powi(31)).log2()); + println!("| P(ROT all equal) | {:.4e} (log2 {:.1}) |", 31f64.powi(-7), (31f64.powi(-7)).log2()); +} + +// ------------------------------------------------------------------------------------------------------------ +// Model B on named days +// ------------------------------------------------------------------------------------------------------------ + +fn scm_refine(days: Vec, threads: usize, label: &str) { + let t0 = Instant::now(); + let scm = Scm::build(threads); + let cl = Classifier::new(); + println!("scm-refine: {} days, {threads} threads, sets built in {:.1} s", days.len(), t0.elapsed().as_secs_f64()); + println!(); + println!("| SCM level (restricted form) | Odd constants | Fraction of 2^31 | log2 |"); + println!("|---|---|---|---|"); + for k in 0..5 { + let f = scm.counts[k] as f64 / 2f64.powi(31); + println!("| {k} | {} | {f:.4e} | {:.1} |", scm.counts[k], if f > 0.0 { f.log2() } else { f64::NEG_INFINITY }); + } + println!(); + // per day: cost B and the per-word certification histogram + let results: Vec<(u64, u32, u32, [u32; 16], Vec>)> = std::thread::scope(|sc| { + let hs: Vec<_> = (0..threads) + .map(|t| { + let scm = &scm; + let cl = &cl; + let days = &days; + sc.spawn(move || { + let mut out = Vec::new(); + for (i, &d) in days.iter().enumerate() { + if i % threads != t { + continue; + } + let mp = params_of_day(d); + let c = cl.classify(&mp); + let cert: Vec> = mp.mul.iter().map(|&m| scm.certify(m)).collect(); + let cost_b: u32 = QR_OPS + cert.iter().zip(c.w.iter()).map(|(k, &w)| k.unwrap_or(u32::MAX).min(w - 1)).sum::(); + out.push((d, c.cost_a, cost_b, c.w, cert)); + } + out + }) + }) + .collect(); + hs.into_iter().flat_map(|h| h.join().unwrap()).collect() + }); + let mut cert_hist: HashMap = HashMap::new(); + let mut words = 0u64; + let mut hist_b = vec![0u64; 512]; + for (_, _, cb, _, cert) in &results { + hist_b[*cb as usize] += 1; + for k in cert { + words += 1; + *cert_hist.entry(k.map(|x| x.to_string()).unwrap_or("none".into())).or_default() += 1; + } + } + println!("## Model B ({label}): {} days, {words} words, {:.1} s", results.len(), t0.elapsed().as_secs_f64()); + println!(); + println!("| Certified adders per word | Words | Fraction |"); + println!("|---|---|---|"); + let mut keys: Vec<&String> = cert_hist.keys().collect(); + keys.sort(); + for k in keys { + println!("| {k} | {} | {:.4e} |", cert_hist[k], cert_hist[k] as f64 / words as f64); + } + println!(); + let n = results.len() as f64; + let mut acc = 0u64; + let mut med_b = 0; + for (c, &h) in hist_b.iter().enumerate() { + acc += h; + if acc as f64 > n / 2.0 { + med_b = c; + break; + } + } + let mean_b: f64 = hist_b.iter().enumerate().map(|(c, &h)| c as f64 * h as f64).sum::() / n; + let min_b = hist_b.iter().position(|&h| h > 0).unwrap_or(0); + println!("| Cost B statistic | Value |"); + println!("|---|---|"); + println!("| mean | {mean_b:.3} |"); + println!("| median | {med_b} |"); + println!("| min | {min_b} |"); + println!(); + println!("| Cost B | Days |"); + println!("|---|---|"); + for (c, &h) in hist_b.iter().enumerate() { + if h > 0 { + println!("| {c} | {h} |"); + } + } + println!(); + let mut sorted = results.clone(); + sorted.sort_by_key(|r| (r.2, r.0)); + println!("Lowest cost B days (day, date, cost A, cost B, w32 per word, certified per word):"); + for (d, ca, cb, w, cert) in sorted.iter().take(24) { + println!( + " {d} {} A={ca} B={cb} w32=[{}] cert=[{}]", + civil(*d), + w.iter().map(|x| x.to_string()).collect::>().join(","), + cert.iter().map(|k| k.map(|x| x.to_string()).unwrap_or("-".into())).collect::>().join(",") + ); + } + println!(); + // certification self-test + println!("| SCM self-test constant | Certified | Expected |"); + println!("|---|---|---|"); + for (c, exp) in [(3u32, "1"), (5, "1"), (7, "1"), (9, "1"), (0xFFFFFFFF, "1"), (0x80000001, "1"), (15, "1 (16-1)"), (45, "2 (9*5)"), (0x9E3779B9, "<= w32-1 or none")] { + println!("| {c:#010x} | {} | {exp} |", scm.certify(c).map(|k| k.to_string()).unwrap_or("none".into())); + } +} + +// ------------------------------------------------------------------------------------------------------------ +// The real calendar and the cross-day structure +// ------------------------------------------------------------------------------------------------------------ + +fn calendar(from: u64, years: u64, threads: usize) { + let t0 = Instant::now(); + let count = (years as f64 * 365.25) as u64; + let cl = Classifier::new(); + let mut tally = Tally::new(32); + let mut seeds: HashMap = HashMap::new(); + let mut muls: HashMap = HashMap::new(); + let mut rcs: HashMap = HashMap::new(); + let mut seed_coll = 0; + let mut mul_coll = Vec::new(); + let mut rc_coll = 0; + let mut days: Vec<(u64, MixParams)> = Vec::with_capacity(count as usize); + for d in from..from + count { + let mp = params_of_day(d); + let c = cl.classify(&mp); + tally.add(d, &c); + let s = seed64(&mp.key); + if seeds.insert(s, d).is_some() { + seed_coll += 1; + } + for (i, &m) in mp.mul.iter().enumerate() { + if let Some(prev) = muls.insert(m, (d, i)) { + mul_coll.push((m, prev, (d, i))); + } + } + for (i, &r) in mp.rc.iter().enumerate() { + if rcs.insert(r, (d, i)).is_some() { + rc_coll += 1; + } + } + days.push((d, mp)); + } + // stream shifts: seed_b = seed_a + k * GAMMA, |k| <= 72 (a day's 40 draws overlap another's) + let mut shifts = Vec::new(); + for (&s, &d) in &seeds { + for k in 1..=72u64 { + let s2 = s.wrapping_add(k.wrapping_mul(GAMMA)); + if let Some(&d2) = seeds.get(&s2) { + shifts.push((d, d2, k)); + } + } + } + println!("calendar: {count} days ({years} years) from {from} ({}), {:.1} s", civil(from), t0.elapsed().as_secs_f64()); + tally.print(from, &format!("Calendar {} to {}", civil(from), civil(from + count - 1))); + println!("## Cross-day structure over the calendar"); + println!(); + println!("| Quantity | Count | Expected |"); + println!("|---|---|---|"); + let pairs = count as f64 * (count as f64 - 1.0) / 2.0; + println!("| 64-bit seed collisions (identical parameters on two days) | {seed_coll} | {:.3e} |", pairs / 2f64.powi(64)); + println!("| stream shifts by k in 1..=72 (one day's draws overlap another's) | {} | {:.3e} |", shifts.len(), pairs * 144.0 / 2f64.powi(64)); + let nm = count as f64 * 16.0; + println!("| MUL value shared by two days (one multiplier block reusable, 0 gain) | {} | {:.1} |", mul_coll.len(), nm * (nm - 1.0) / 2.0 / 2f64.powi(31)); + println!("| RC value shared by two days | {rc_coll} | {:.1} |", nm * (nm - 1.0) / 2.0 / 2f64.powi(32)); + for (m, a, b) in mul_coll.iter().take(8) { + println!("| shared MUL {m:08x} | day {} word {} and day {} word {} | |", a.0, a.1, b.0, b.1); + } + for (a, b, k) in shifts.iter().take(8) { + println!("| shift: day {a} and day {b} by {k} | | |"); + } + println!(); + // the ten worst days under A with dates, and under C + let mut by_a: Vec<(u32, u64)> = days.iter().map(|(d, mp)| (cl.classify(mp).cost_a, *d)).collect(); + by_a.sort_unstable(); + println!("| Rank | Day | Date | Cost A | Gain A |"); + println!("|---|---|---|---|---|"); + for (i, (c, d)) in by_a.iter().take(10).enumerate() { + println!("| {} | {d} | {} | {c} | {:.4}x |", i + 1, civil(*d), MEDIAN_A() as f64 / *c as f64); + } + println!(); + let mut by_c: Vec<(u32, u64)> = days.iter().map(|(d, mp)| (cl.classify(mp).k_c, *d)).filter(|x| x.0 > 0).collect(); + by_c.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); + println!("| Model C days (k >= 1) | Day | Date | k | Gain C |"); + println!("|---|---|---|---|---|"); + if by_c.is_empty() { + println!("| none | | | | |"); + } + for (i, (k, d)) in by_c.iter().take(10).enumerate() { + println!("| {} | {d} | {} | {k} | {:.4}x |", i + 1, civil(*d), 16.0 / (16.0 - *k as f64)); + } + println!(); + println!("Planted shift check: two synthetic seeds s and s + GAMMA share draws 1..39 of the first with 0..38 of the second:"); + let mut r1 = SplitMix64::new(0x1234_5678_9abc_def0); + let mut r2 = SplitMix64::new(0x1234_5678_9abc_def0u64.wrapping_add(GAMMA)); + let _ = r1.next(); + let a = r1.next(); + let b = r2.next(); + println!(" draw 1 of s = {a:016x}, draw 0 of s + GAMMA = {b:016x}, equal = {}", a == b); + println!(); + let _ = threads; +} + +// ------------------------------------------------------------------------------------------------------------ +// Avalanche of k applications and of the address bits +// ------------------------------------------------------------------------------------------------------------ + +fn avalanche(mp: &MixParams, label: &str, states: usize) { + let rks = round_keys(); + let mut rng = SplitMix64::new(0xA5A5_0000_0000_0001 ^ seed64(&mp.key)); + println!("### Avalanche: {label}, {states} states, flips of each of 512 input bits"); + println!(); + println!("| Applications k | Mean output flip fraction | Min per output bit | Max per output bit | Address bits (22 of s[0]) mean flip | Address min per bit | Output bits never flipped by some input bit |"); + println!("|---|---|---|---|---|---|---|"); + for k in [1usize, 2, 3, 4, 8] { + // flips[in][out] + let mut flips = vec![[0u32; 512]; 512]; + for _ in 0..states { + let mut s = [0u32; 16]; + for w in s.iter_mut() { + *w = rng.next() as u32; + } + let mut base = s; + for j in 0..k { + mixer(&mut base, rks[j], mp); + } + for ib in 0..512 { + let mut t = s; + t[ib >> 5] ^= 1u32 << (ib & 31); + for j in 0..k { + mixer(&mut t, rks[j], mp); + } + for ow in 0..16 { + let x = t[ow] ^ base[ow]; + if x != 0 { + for ob in 0..32 { + if x >> ob & 1 == 1 { + flips[ib][ow * 32 + ob] += 1; + } + } + } + } + } + } + let total = (states * 512 * 512) as f64; + let mut sum = 0u64; + let mut per_out = vec![0u64; 512]; + let mut minf = f64::MAX; + let mut maxf = 0.0f64; + let mut never = 0u64; + let mut addr_sum = 0u64; + let mut addr_min = f64::MAX; + for ib in 0..512 { + for ob in 0..512 { + let f = flips[ib][ob]; + sum += f as u64; + per_out[ob] += f as u64; + let fr = f as f64 / states as f64; + minf = minf.min(fr); + maxf = maxf.max(fr); + if f == 0 { + never += 1; + } + if ob < 22 { + addr_sum += f as u64; + addr_min = addr_min.min(fr); + } + } + } + println!("| {k} | {:.4} | {minf:.4} | {maxf:.4} | {:.4} | {addr_min:.4} | {never} |", sum as f64 / total, addr_sum as f64 / (states * 512 * 22) as f64); + } + println!(); +} + +// ------------------------------------------------------------------------------------------------------------ +// Redraw rule census +// ------------------------------------------------------------------------------------------------------------ + +/// The proposed rule: draw ROT, MUL, RC; if the day's cost A is below `max_cost` or any word is in the exact +/// 2-adder set, continue the same stream and draw all forty again; repeat until accepted. The honest miner pays +/// forty more SplitMix64 draws per redraw, once a day. +fn redraw_params(key: [u32; 8], shape: Shape, cl: &Classifier, max_cost: u32) -> (MixParams, u32) { + let mut rng = SplitMix64::new(seed64(&key)); + let mut redraws = 0; + loop { + let mut rot = [0u32; 8]; + let mut mul = [0u32; 16]; + let mut rc = [0u32; 16]; + for r in rot.iter_mut() { + *r = 1 + rng.below(31) as u32; + } + for m in mul.iter_mut() { + *m = (rng.next() as u32) | 1; + } + for c in rc.iter_mut() { + *c = rng.next() as u32; + } + let mut mp = MixParams::with_shape(key, shape); + mp.rot = rot; + mp.mul = mul; + mp.rc = rc; + let c = cl.classify(&mp); + if c.cost_a >= max_cost && c.k_c == 0 && !c.flags[0] { + return (mp, redraws); + } + redraws += 1; + } +} + +fn redraw_census(from: u64, count: u64, threads: usize, max_cost: u32) { + let t0 = Instant::now(); + let cl = Classifier::new(); + let shape = v4_shape(); + // check: redraw_params with max_cost 0 reproduces the real draw on every day of a sample + for d in from..from + 1000 { + let (mp, n) = redraw_params(key_of_day(d), shape, &cl, 0); + let real = params_of_day(d); + assert_eq!(n, 0); + assert_eq!((mp.rot, mp.mul, mp.rc), (real.rot, real.mul, real.rc), "redraw at max_cost 0 must be the real draw"); + } + let (tallies, redraw_hist): (Vec, Vec<[u64; 8]>) = std::thread::scope(|sc| { + let hs: Vec<_> = (0..threads) + .map(|t| { + let cl = &cl; + sc.spawn(move || { + let mut tally = Tally::new(16); + let mut rh = [0u64; 8]; + let chunk = count.div_ceil(threads as u64); + let lo = from + t as u64 * chunk; + let hi = (lo + chunk).min(from + count); + for d in lo..hi { + let (mp, n) = redraw_params(key_of_day(d), shape, cl, max_cost); + rh[(n as usize).min(7)] += 1; + let c = cl.classify(&mp); + tally.add(d, &c); + } + (tally, rh) + }) + }) + .collect(); + hs.into_iter().map(|h| h.join().unwrap()).unzip() + }); + let mut all = Tally::new(16); + for t in tallies { + all.merge(t); + } + let mut rh = [0u64; 8]; + for h in redraw_hist { + for i in 0..8 { + rh[i] += h[i]; + } + } + println!("redraw-census: rule = redraw while cost A < {max_cost} or model C k >= 1 or ROT all equal; {count} days from {from}, {:.1} s", t0.elapsed().as_secs_f64()); + println!(); + println!("| Redraws needed | Days | Fraction |"); + println!("|---|---|---|"); + for i in 0..8 { + if rh[i] > 0 { + println!("| {}{} | {} | {:.3e} |", i, if i == 7 { "+" } else { "" }, rh[i], rh[i] as f64 / count as f64); + } + } + println!(); + all.print(from, "After the redraw rule"); +} + +// ------------------------------------------------------------------------------------------------------------ +// main +// ------------------------------------------------------------------------------------------------------------ + +fn arg(args: &[String], name: &str) -> Option { + args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned()) +} +fn arg_u64(args: &[String], name: &str, default: u64) -> u64 { + arg(args, name).map(|s| if let Some(e) = s.strip_prefix("2^") { 1u64 << e.parse::().unwrap() } else { s.parse().unwrap() }).unwrap_or(default) +} + +fn main() { + let args: Vec = std::env::args().collect(); + let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("help"); + let threads = arg_u64(&args, "--threads", 8) as usize; + MEDIAN_A_CELL.store(arg_u64(&args, "--median", 226) as u32, Ordering::Relaxed); + println!("adv-mixer-2 {} | internal adversarial pass, not an independent review | class v4 x8, {} applications per item | median cost A for gains {}", args[1..].join(" "), APPS_PER_ITEM, MEDIAN_A()); + println!(); + match cmd { + "day" => { + let d = arg_u64(&args, "--index", GENESIS_DAY); + let cl = Classifier::new(); + print_day(&format!("day {d} ({})", civil(d)), ¶ms_of_day(d), &cl, None); + } + "plant" => { + let cl = Classifier::new(); + let kinds: Vec<&str> = if args.len() > 2 { args[2..].iter().map(|s| s.as_str()).collect() } else { vec!["alleq", "mul1", "mul1all", "rcrk0", "rot1", "weakday"] }; + print_day(&format!("day {GENESIS_DAY} unplanted"), ¶ms_of_day(GENESIS_DAY), &cl, None); + for k in kinds { + print_day(&format!("plant {k} on day {GENESIS_DAY}"), &plant(k), &cl, None); + } + } + "census" => { + let from = arg_u64(&args, "--from", GENESIS_DAY); + let count = arg_u64(&args, "--count", 1 << 24); + let cap = arg_u64(&args, "--lowest", 64) as usize; + census(from, count, threads, cap, arg(&args, "--out-days")); + } + "exact" => exact(threads), + "scm-refine" => { + let (days, label): (Vec, String) = if let Some(f) = arg(&args, "--days") { + let file = std::fs::File::open(&f).unwrap(); + let v: Vec = std::io::BufReader::new(file).lines().filter_map(|l| l.ok()).filter_map(|l| l.split_whitespace().next().and_then(|x| x.parse().ok())).collect(); + (v, format!("days from {f}")) + } else if let Some(n) = arg(&args, "--sample") { + let n: u64 = n.parse().unwrap(); + let seed = arg_u64(&args, "--seed", 1); + let mut r = SplitMix64::new(seed); + let v: Vec = (0..n).map(|_| GENESIS_DAY + r.below(1 << 32)).collect(); + (v, format!("random sample of {n} days in [genesis, genesis + 2^32), seed {seed}")) + } else { + let from = arg_u64(&args, "--from", GENESIS_DAY); + let count = arg_u64(&args, "--count", 36_525); + ((from..from + count).collect(), format!("{count} days from {from} ({})", civil(from))) + }; + scm_refine(days, threads, &label); + } + "calendar" => { + let from = arg_u64(&args, "--from", GENESIS_DAY); + let years = arg_u64(&args, "--years", 100); + calendar(from, years, threads); + } + "avalanche" => { + let states = arg_u64(&args, "--states", 1024) as usize; + let d = arg_u64(&args, "--index", GENESIS_DAY); + let mp = match arg(&args, "--plant") { + Some(p) => plant(&p), + None => params_of_day(d), + }; + let label = match arg(&args, "--plant") { + Some(p) => format!("plant {p} on day {GENESIS_DAY}, ROT {:?}", mp.rot), + None => format!("day {d} ({}), ROT {:?}", civil(d), mp.rot), + }; + avalanche(&mp, &label, states); + } + "redraw-census" => { + let from = arg_u64(&args, "--from", GENESIS_DAY); + let count = arg_u64(&args, "--count", 1 << 24); + let max_cost = arg_u64(&args, "--max-cost", 211) as u32; + redraw_census(from, count, threads, max_cost); + } + _ => { + eprintln!("commands: day, plant, census, exact, scm-refine, calendar, avalanche, redraw-census (see the file header)"); + std::process::exit(2); + } + } +}