From 36ef07fda24516ef592e4b8ee2857df7ff961114 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:21:25 +0000 Subject: [PATCH] mixer x4: tests/mixer.rs (200-program v3 fuzz with the Metal pack writer, stats beside v2, dataset edges at every multiplier, determinism against the pinned pack); scratch.rs edge literal gains era_bytes; mixer-x4.md: the v3 vector tables, the v2 diff, the Metal and Apple OpenCL bit-exactness rows, the soundness table so far Co-Authored-By: Claude Fable 5.1 --- docs/plans/mixer-x4.md | 104 +++++++++++++++- igneum-pow/tests/mixer.rs | 237 ++++++++++++++++++++++++++++++++++++ igneum-pow/tests/scratch.rs | 1 + 3 files changed, 339 insertions(+), 3 deletions(-) create mode 100644 igneum-pow/tests/mixer.rs diff --git a/docs/plans/mixer-x4.md b/docs/plans/mixer-x4.md index 2635da058..3f812f562 100644 --- a/docs/plans/mixer-x4.md +++ b/docs/plans/mixer-x4.md @@ -125,8 +125,57 @@ node agent). ## 4. Vectors (class v3, `proto-cuda/packs-ca2-mixer/`) -Filled in section 6 from the exported packs: `mx4-genesis` (seed `igneum-genesis`, day `2026-10-03`, 2^28 words, -2^26-word cache) and `mx4-devnet-epoch0` (the devnet genesis hash as the epoch seed, day bytes of 2026-10-04). +Produced by `igneum-pow export --program-class v3` (the Rust CPU interpreter, 5 October 2026, commit 66eeba3) and +checked on the GPUs in section 6. The program of each pack is the version 2 program of the same seed instruction +for instruction (`tests/packs.rs`, `v3_packs_are_the_v2_seeds_under_mixer_x4`); the cache is the version 2 cache +(day 0 of the growth rule); the dataset words and the hashes are new. The 64 sampled indices are those of every +pack (`emit::sample_indices`). + +Pack `mx4-genesis` (seed igneum-genesis, day 2026-10-03, generator 3, class mx4, program id e323b9dcaf283a6f, 2^28 words, 2^26-word cache, cache FNV-1a 64 `48c4f5bf24166b2e` as under v2): + +``` +dataset words 0..15 (item 0): + 61ff2180 0d4c7e6c 2177d443 60df9025 cf8b2e10 63675bfb 25289e58 9c45dc42 + 2d271c54 9652369b 2dd77508 5921392c 3afa60ee c640ad68 f2bb56ff cfa46438 +dataset[0x0fffffff] = 5020180e +sampled words (the first 8 of the 64 in vectors.json): + dataset[59471966] = de85726d dataset[217795994] = 7cfc31c7 dataset[208353206] = d3cd5289 dataset[42483309] = 6ebbeef1 + dataset[172547758] = 9858413b dataset[148076330] = e786f141 dataset[183853158] = 64f13833 dataset[214389424] = ca229d04 +unit at base nonce 0, lanes 0..31: + 63acd2d273f475ba e929c78b34b80d4b 0b1011cb19982558 1457a0df5497aa11 + 957d0f3bb71d98fb ac16901e6e6f6057 8ea1c6279f4b177a f28146e60bd08ba9 + fe5b8cfe87f8e65b 49f87240566ace62 6ef6d6b7bdea8e41 46d9c0dc29a97b9c + 111fe30128db9398 66dc39084f0946d4 8ee11bdfd35fecf2 2861fcfc75db6677 + 31c7667d4bde8556 c5989c48858b4ce0 276395e734a9d30d 84217b41e91368ff + 3604861e34d9f697 9f51d8ee16bf3639 c89e47bafa84401c 7ae78c1f10b70e19 + 0b8c947157a29a48 d67192e8cfb43842 05a4c6d182c8c675 188e2661f3263f2e + a2df24238f7fea2e ed69ea7e13ad3a48 2d44ae509bab91b8 adad61931ea4fb70 +unit at base nonce 4096: lane 0 edd508ac57e5699a, lane 1 4eefd56d526cdaeb, lane 31 8892f8604733b1e0 +unit at base nonce 1000000: lane 0 8b3183778a49f59c, lane 1 1831b72a8797e895, lane 31 75eae55eba53a506 +``` + +Pack `mx4-devnet-epoch0` (seed the devnet genesis hash, day bytes of 2026-10-04, generator 3, class mx4, program id 73bcbfe8ccf988f1, 2^28 words, 2^26-word cache, cache FNV-1a 64 `448274a57f508cbc` as under v2): + +``` +dataset words 0..15 (item 0): + afe80d67 b9fbd029 6c79f193 95139ad9 96310aff 4609f8b1 75279e63 28235be1 + 47b17dcb 718e0ef2 a52588c8 a8bf49d5 19cf243e 5ec8905e a4851f66 af9cd9f3 +dataset[0x0fffffff] = e6a99c7a +sampled words (the first 8 of the 64 in vectors.json): + dataset[59471966] = 57642b58 dataset[217795994] = c279badd dataset[208353206] = cbccbaad dataset[42483309] = 32cce392 + dataset[172547758] = 71fdb4c6 dataset[148076330] = f5a268ce dataset[183853158] = 3ca1d676 dataset[214389424] = 7977b03d +unit at base nonce 0, lanes 0..31: + 212c6442b51e87ae c374795c00839331 b6036a220a98f4b3 eb8b8013e637367b + db5866e9b73930fd f3f3d01f46e90333 9d913991ab8ed428 7ccb1d8fa100a800 + 3cf45ba44f09a0fe 91acf48ef1a63082 6ea46c69fb082f99 581f0218977a9d72 + 9a4623a5c62ddf2d ab6eb5e768f0feb4 07b70bdccf8aca12 d666311ae5e4311e + 53114757d669f0a4 bd5d6ace87ce2ce4 fb712015e8189192 a32cec81103e134b + 83f3d18c3289c124 fe29f1984b132b3d c9ffcf4e3774497a ac99c9243dc63809 + d78a7e8217a32f3c ab81ad63d242fc31 0e4c30b7e00024af ce014289fff6778d + 64a1292e2a8b4a91 d5b8c90e681d7e3a 06078117673030fd 51bf77b280173930 +unit at base nonce 4096: lane 0 3c797978566b5950, lane 1 7c759e60185b6411, lane 31 96a903eb9a0ca390 +unit at base nonce 1000000: lane 0 d5a8da0568df8ee7, lane 1 68cb69c04208285a, lane 31 f8ca84a1a5d78cf5 +``` ## 5. The v2 path is byte-identical @@ -136,7 +185,56 @@ section 6 also records a fresh `igneum-pow export` of both packs diffed against ## 6. Measurements -Filled as they land. Every row names the machine, the date, the command and the lock mode. +Machine: Apple M5 Max, 64 GiB, Darwin 25.6.0, 5 October 2026 (night), other agents' builds running beside every +run; a timing row says which lock it ran under (`measure` is exclusive; `run` and `build` are not timings). + +### 6.1 The v2 path, byte for byte (no lock needed) + +`igneum-pow export --seed igneum-genesis --day 2026-10-03` and `igneum-pow export --epoch-hex edc4fa84...fb07 +--day-hex 69676e65756d2d6461792ffa50000000000000` on commit 66eeba3, `diff -r` against +`proto-cuda/packs/igneum-genesis-mh` and `igneum-devnet-v4-epoch0`: IDENTICAL, both (twelve files each). The +crate tests regenerate the same files and compare them on every run (`tests/packs.rs`, 12 of 12 pass). + +### 6.2 Bit-exactness of the class v3 construction on the GPUs (`with-lock.sh run`, 22:05 UTC) + +`packbench --pack --batches 1 --batch-log2 24 --group 256` (Metal, built from this branch) and +`igneum-bench-cl-igneum-genesis-mh --bench-pack --pack --batches 1 --batch-log2 24` (Apple OpenCL, built from +this branch's host.c). Vectors are the Rust interpreter's; the fingerprint is FNV-1a 64 over the 2^24 outputs at +base nonce 0. + +| Pack | Harness | Cache FNV-1a 64 | Dataset head, word MASK, 64 samples | Vectors standalone / in batch | Fingerprint 2^24 | MH/s (GPU time; not a measurement, the run lock) | +|---|---|---|---|---|---|---| +| mx4-genesis | Metal | 48c4f5bf24166b2e PASS | PASS | 3/3, 3/3 | 6f48d5a2aa0dbe5f | 27.6 | +| mx4-genesis | Apple OpenCL | PASS | PASS | 96 of 96 lanes | 6f48d5a2aa0dbe5f | 27.7 (wall) | +| mx4-devnet-epoch0 | Metal | 448274a57f508cbc PASS | PASS | 3/3, 3/3 | 73caaebb28e808fe | 27.5 | +| mx4-devnet-epoch0 | Apple OpenCL | PASS | PASS | 96 of 96 lanes | 73caaebb28e808fe | 27.6 (wall) | + +Reading: the Rust interpreter, Metal and Apple OpenCL agree on the v3 dataset (head, MASK word, 64 samples), on +every vector lane and on the 2^24-output fingerprint of each pack; the hash rate is the v2 rate (27.7 MH/s on this +card tonight, readwidth table), as it must be: the hash kernel only loads, the mixer is paid in the build. + +Dataset build under the run lock (indicative only; the measured rows are in 6.4): Metal 30.2 ms GPU (mx4-genesis) +and 21.7 ms GPU (mx4-devnet-epoch0) for 1 GiB; Apple OpenCL 55 and 56 ms wall. The readwidth entry's v2 figure on +this card is 0.6 to 2.1 ms cache fill and a 1 GiB build the bench-log's memory-hard entry puts at 13 to 30 ms; the +x4 build on the GPU is the row the measure lock will settle. + +### 6.3 Soundness suite on the v3 construction (commit 66eeba3 and after; `with-lock.sh build` for cargo, `run` for the GPU) + +| Suite | Command | Result | +|---|---|---| +| Crate lib tests (memhard schedule table, the by-hand multiplied mixer, the seam, the generator) | `cargo test -j4 --release` | 44 of 44 pass | +| Pinned packs, v2 and v3 (`tests/packs.rs`: programs, ids, dataset words, 96 vectors per pack, every emitted file byte for byte, 16 masked loads per kernel, the v3 packs as the v2 seeds under mixer x4) | same | 12 of 12 pass | +| Scratch soundness tests of branch ca2-soundness (cherry-pick 0d8f745, one conflict in packbench.swift's RESULT line resolved by hand, `era_bytes: None` added to the edge program literal) | `cargo test -j4 --release --test scratch` | 7 of 7 pass: bijections, re-hit rates, 56 of 56 edge units, 42 of 42 emitted scr kernels, 200 scratch programs and 800 units on the CPU | +| v3 fuzz on the CPU (`tests/mixer.rs`): 200 programs through the seam, the contract on every instruction (each is the v2 program of its seed), 4 units each across the 32-bit range with one unit in the top 256 nonces, interpreted twice | `IGNEUM_MIXER_PACKS_OUT= cargo test -j4 --release --test mixer` | 200 of 200, 800 of 800 units; 200 packs written for the GPU runs (82 s with the three cache fills) | +| v3 stats beside v2 (`tests/mixer.rs`): 8,192 outputs per seed, bit balance, single-bit avalanche within the unit and across units, duplicates | same | igneum-genesis v3: avalanche 49.99 percent, worst bit z 1.92, 0 duplicates (v2: 49.87, z 2.25); igneum-genesis/stats1 v3: 49.97, z 3.09 (v2: 49.98, z 2.30) | +| v3 edge (`tests/mixer.rs`): items 0, 1, 2^28 - 1 and 2^32 - 1 by hand at m = 1, 2, 4, 8 on a 2^14-word cache; words 0, 15, 16, 17, MASK - 1, MASK through the interpreter's fetch path; the index wrap at MASK + 1 | same | pass | +| v3 determinism (`tests/mixer.rs`): two independent epochs, every vector and every emitted file equal, and equal to the pinned pack | same | pass | +| Metal and Apple OpenCL on the two pinned v3 packs | section 6.2 | 3/3 standalone, 3/3 in batch, 96 of 96 lanes, dataset head, MASK word and 64 samples, one fingerprint per pack across both harnesses | +| Metal fuzz: the 200 packs, 4 units each standalone and the top-256 unit inside a 512-nonce batch at base 4,294,967,040; every tenth pack on Apple OpenCL as well | `packbench --pack --batches 1 --batch-log2 9 --batch-base 4294967040` | see below | + +### 6.4 Timings (`with-lock.sh measure`) + +Owed until the measure lock frees. ## 7. The chip model diff --git a/igneum-pow/tests/mixer.rs b/igneum-pow/tests/mixer.rs new file mode 100644 index 000000000..bd40c1881 --- /dev/null +++ b/igneum-pow/tests/mixer.rs @@ -0,0 +1,237 @@ +//! The class v3 dataset construction (mixer x4, cache growth option C; `docs/plans/mixer-x4.md`): the soundness +//! runs the brief asks for, on the CPU, with the packs for the GPU runs written on request. +//! +//! 1. Fuzz: `IGNEUM_MIXER_FUZZ` (default 200) programs through the seam (`ProgramClass::V3`), the contract on every +//! instruction (the v2 program of the seed, instruction for instruction), 4 units each across the 32-bit range +//! including the wrap, interpreted twice on the CPU; with `IGNEUM_MIXER_PACKS_OUT=` every program is written +//! as a pack with its 4 bases in vectors.json for `packbench` and the OpenCL host (the Metal fuzz). +//! 2. Stats: bit balance and single-bit-flip avalanche of the v3 hash against v2 on the same programs and nonces. +//! 3. Edge: the dataset at word 0, word MASK and the item boundary, derived through the interpreter's fetch path and +//! by hand at every multiplier 1, 2, 4, 8, on a small cache. +//! 4. Determinism: two independent epochs of the same seed and day agree on every vector and every emitted file. + +use igneum_pow::emit::{export_pack, vectors_json}; +use igneum_pow::generator::{generate_from_seed_bytes, generate_from_seed_bytes_program_class, Op, Program, ProgramClass, GENERATOR_VERSION_V3, INSTR_COUNT, V3_CLASS}; +use igneum_pow::memhard::{derive_item, mixer, round_key, Cache, MixParams, Shape}; +use igneum_pow::seed::{day_key, SplitMix64}; +use igneum_pow::verify::{DatasetMode, DatasetSource, Epoch}; +use std::collections::HashMap; +use std::path::PathBuf; + +const DAY: &str = "2026-10-03"; + +fn contract(p: &Program, seed: &str) { + assert_eq!(p.generator, GENERATOR_VERSION_V3); + assert_eq!(p.class, V3_CLASS); + assert_eq!(p.instrs.len(), INSTR_COUNT); + assert_eq!(p.instrs.iter().filter(|i| i.op == Op::Load).count(), 16); + for (k, i) in p.instrs.iter().enumerate() { + assert!(i.src != i.dst, "#{k}: src == dst"); + assert!((1..=31).contains(&i.rot), "#{k}: rot {}", i.rot); + assert!([1u8, 2, 4, 8, 16].contains(&i.mask), "#{k}: mask {}", i.mask); + assert!(i.dst < 8 && i.src < 8 && i.src2 < 8); + assert_eq!(i.width, 1, "#{k}: a v3 load reads one word"); + } + assert!(igneum_pow::accept::check(p).is_ok(), "an accepted program"); + let v2 = generate_from_seed_bytes(seed, seed.as_bytes()); + assert_eq!(p.instrs, v2.instrs, "the v2 program of the seed under the v3 construction"); + assert_eq!(p.attempt, v2.attempt); +} + +/// Write a pack whose vectors.json carries `bases` instead of the three standard bases (packbench and the OpenCL +/// host check every unit standalone and the ones inside the batch window). +fn write_pack_with_bases(dir: &PathBuf, e: &Epoch, day: &str, bases: &[u32], source: &str) { + let mut pack = export_pack(e, day, source); + let outs: Vec<[u64; 32]> = bases.iter().map(|&b| e.hash_warp(b)).collect(); + let vj = vectors_json(&e.program, day, e.dataset.log2_words, bases, &outs, &pack.vectors, e.dataset.mask, source, true); + for f in pack.files.iter_mut() { + if f.0 == "vectors.json" { + f.1 = vj.clone(); + } + } + pack.write_to(dir).unwrap(); +} + +#[test] +fn fuzz_v3_programs_cpu() { + let n: usize = std::env::var("IGNEUM_MIXER_FUZZ").ok().and_then(|s| s.parse().ok()).unwrap_or(200); + let out = std::env::var("IGNEUM_MIXER_PACKS_OUT").ok().map(PathBuf::from); + let mut rng = SplitMix64::new(0x6967_6e65_756d_2d6d); // "igneum-m" + let shape = Shape::for_class(&V3_CLASS); + assert_eq!(shape, Shape { mixer_mult: 4, cache_log2_words: 26 }); + // one memory-hard source per dataset size (the 256 MiB cache fill is 0.2 s each) + let mut mh: HashMap = HashMap::new(); + let mut manifest = String::from("pack\tlog2\tprogram_id\tbases\n"); + let mut units = 0usize; + let mut wraps = 0usize; + for i in 0..n { + let seed = format!("igneum-mixer-fuzz/{i}"); + let p = generate_from_seed_bytes_program_class(&seed, seed.as_bytes(), ProgramClass::V3, None); + contract(&p, &seed); + let b0 = (rng.below(8) as u32) * 32; + let b1 = 0x8000_0000u32.wrapping_sub(256).wrapping_add((rng.below(16) as u32) * 32); + let b2 = 0xffff_ff00u32.wrapping_add((rng.below(8) as u32) * 32); + let b3 = (rng.next() as u32) & !31; + let bases = [b0, b1, b2, b3]; + wraps += bases.iter().filter(|&&b| b >= 0xffff_ff00).count(); + let log2 = [24u32, 26, 28][rng.below(3) as usize]; + let ds = mh.remove(&log2).unwrap_or_else(|| DatasetSource::new_shape(DAY, DatasetMode::MemoryHard, log2, shape)); + let e = Epoch { program: p, dataset: ds }; + for &b in &bases { + let r1 = e.interpret_warp(b); + let r2 = e.interpret_warp(b); + assert_eq!(r1.hashes, r2.hashes); + assert!(r1.items_derived >= 120 * 32 / 32 && r1.items_derived <= 4_096, "{seed}: {} items", r1.items_derived); + units += 1; + } + if let Some(dir) = &out { + let pack_name = format!("fuzz-{i:03}-mx4-l{log2}"); + write_pack_with_bases(&dir.join(&pack_name), &e, DAY, &bases, "igneum-pow tests/mixer.rs fuzz"); + manifest.push_str(&format!( + "{pack_name}\t{log2}\t{:016x}\t{}\n", + e.program.program_id(), + bases.iter().map(|b| format!("{b}")).collect::>().join(",") + )); + } + mh.insert(log2, e.dataset); + } + println!("fuzz: {n} v3 programs, {units} units on the CPU, {wraps} units in the top 256 nonces"); + assert_eq!(units, 4 * n); + assert_eq!(wraps, n); + if let Some(dir) = &out { + std::fs::create_dir_all(dir).unwrap(); + std::fs::write(dir.join("manifest.tsv"), manifest).unwrap(); + println!("packs written to {}", dir.display()); + } +} + +/// Bit balance and avalanche of the v3 hash beside v2 on the same program (the TESTS.md section 3 shape, on the +/// CPU, 2^13 nonces per seed): every output bit within 5 sigma of half ones; a single nonce-bit flip moves 50 percent +/// of the output bits within 2 points; no duplicate among the outputs. +#[test] +fn stats_v3_against_v2() { + let n_warps = 256usize; // 8,192 nonces + for seed in ["igneum-genesis", "igneum-genesis/stats1"] { + let v3 = Epoch { + program: generate_from_seed_bytes_program_class(seed, seed.as_bytes(), ProgramClass::V3, None), + dataset: DatasetSource::new_shape(DAY, DatasetMode::MemoryHard, 24, Shape::for_class(&V3_CLASS)), + }; + let v2 = Epoch::new(seed, DAY, DatasetMode::MemoryHard, 24); + for (name, e) in [("v3", &v3), ("v2", &v2)] { + let mut ones = [0u64; 64]; + let mut outs = Vec::with_capacity(n_warps * 32); + for w in 0..n_warps { + let h = e.hash_warp(w as u32 * 32); + for &x in &h { + outs.push(x); + for b in 0..64 { + ones[b] += (x >> b) & 1; + } + } + } + let total = (n_warps * 32) as f64; + let sigma = (total / 4.0).sqrt(); + for (b, &c) in ones.iter().enumerate() { + let z = (c as f64 - total / 2.0).abs() / sigma; + assert!(z < 5.0, "{seed} {name}: bit {b} ones {c} of {total}, z {z:.2}"); + } + // avalanche: flip one bit of the nonce within the unit (lanes 0..31 differ in the low 5 bits) and across + // units (bit 5 and up): compare lane l of unit u with lane l ^ (1 << k) and with unit u ^ (1 << k) + let mut flips = 0u64; + let mut moved = 0u64; + for w in 0..64usize { + let h = e.hash_warp(w as u32 * 32); + for k in 0..5 { + for l in 0..32usize { + moved += (h[l] ^ h[l ^ (1 << k)]).count_ones() as u64; + flips += 1; + } + } + let h2 = e.hash_warp((w ^ 1) as u32 * 32); + for l in 0..32usize { + moved += (h[l] ^ h2[l]).count_ones() as u64; + flips += 1; + } + } + let avg = moved as f64 / flips as f64 / 64.0 * 100.0; + assert!((avg - 50.0).abs() < 2.0, "{seed} {name}: avalanche {avg:.2} percent"); + outs.sort_unstable(); + let dups = outs.windows(2).filter(|p| p[0] == p[1]).count(); + assert_eq!(dups, 0, "{seed} {name}: duplicate outputs"); + println!("{seed} {name}: {} outputs, avalanche {avg:.2} percent, worst bit z {:.2}", outs.len(), ones.iter().map(|&c| (c as f64 - total / 2.0).abs() / sigma).fold(0.0, f64::max)); + } + assert_ne!(v3.hash_warp(0), v2.hash_warp(0)); + } +} + +/// The dataset edges under every multiplier on a small cache: word 0, word MASK, the last word of item 0 and the +/// first of item 1, through `DatasetSource::word` and by hand. +#[test] +fn edge_items_every_multiplier() { + let key = day_key(DAY); + let cache = Cache::fill_log2(key, 14); + for m in [1u32, 2, 4, 8] { + let mp = MixParams::with_shape(key, Shape { mixer_mult: m, cache_log2_words: 14 }); + let by_hand = |t: u32| -> [u32; 16] { + let mut s = [0u32; 16]; + s[..8].copy_from_slice(&key); + for i in 0..8 { + s[8 + i] = t.wrapping_mul(mp.mul[i]).wrapping_add(mp.rc[i]); + } + for r in 0..8usize { + for j in 0..m as usize { + mixer(&mut s, round_key(r * m as usize + j), &mp); + } + let line = cache.line(s[0]); + for i in 0..16 { + s[i] ^= line[i]; + } + } + for j in 0..m as usize { + mixer(&mut s, round_key(8 * m as usize + j), &mp); + } + s + }; + for t in [0u32, 1, 0x0fff_ffff, 0xffff_ffff] { + assert_eq!(derive_item(t, &mp, &cache), by_hand(t), "m {m} item {t}"); + } + } + // the interpreter's fetch path at the genesis cache: words 0, 15, 16 and MASK of a 2^20-word dataset agree with + // the item derivation, under v3 + let ds = DatasetSource::new_shape(DAY, DatasetMode::MemoryHard, 20, Shape::for_class(&V3_CLASS)); + let m = ds.memhard().unwrap(); + for w in [0u32, 15, 16, 17, ds.mask - 1, ds.mask] { + assert_eq!(ds.word(w), derive_item(w >> 4, &m.params, &m.cache)[(w & 15) as usize]); + assert_eq!(ds.word(w), m.word(w)); + } + // a load at an out-of-range register masks to the dataset: the word at mask + 1 is the word at 0 + assert_eq!(ds.word(ds.mask.wrapping_add(1)), ds.word(0)); +} + +/// Two independent epochs of the same seed and day: every vector and every emitted file identical; the pinned v3 +/// pack is what a third export writes. +#[test] +fn determinism_v3() { + let build = || Epoch { + program: generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V3, None), + dataset: DatasetSource::new_shape(DAY, DatasetMode::MemoryHard, 28, Shape::for_class(&V3_CLASS)), + }; + let a = build(); + let b = build(); + let pa = export_pack(&a, DAY, "a"); + let pb = export_pack(&b, DAY, "a"); + assert_eq!(pa.outs, pb.outs); + assert_eq!(pa.vectors, pb.vectors); + assert_eq!(pa.files, pb.files); + for (w, warp) in [(0u32, 0usize), (4096, 1), (1_000_000, 2)] { + assert_eq!(a.hash_warp(w), pa.outs[warp]); + } + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda/packs-ca2-mixer/mx4-genesis"); + for (name, text) in &pa.files { + if name == "vectors.json" || name == "vectors.h" { + continue; // the source string differs ("a" here) + } + let on_disk = std::fs::read_to_string(dir.join(name)).unwrap(); + assert_eq!(&on_disk, text, "{name}"); + } +} diff --git a/igneum-pow/tests/scratch.rs b/igneum-pow/tests/scratch.rs index d00bbfe4e..75e48f583 100644 --- a/igneum-pow/tests/scratch.rs +++ b/igneum-pow/tests/scratch.rs @@ -261,6 +261,7 @@ fn edge(name: &str, c: LoadClass, instrs: Vec) -> Program { generator: GENERATOR_VERSION, attempt: 0, class: c, + era_bytes: None, instrs, } }