diff --git a/docs/plans/igneum-2.0-test-harness-map.md b/docs/plans/igneum-2.0-test-harness-map.md index 5711eb938..4d401d387 100644 --- a/docs/plans/igneum-2.0-test-harness-map.md +++ b/docs/plans/igneum-2.0-test-harness-map.md @@ -408,6 +408,14 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover - Cases: - ADV-06 Separate process advantage from specialisation: partial: the placed gated cores and the adversary's forms are modelled in shadow-k.md; the independent review remains +### kit:class-v6-fingerprints + +- Command: `KITS_BOX_SCRIPT=kits-v6-on-box.sh tools/class-v5/kits-remote.sh --box 1 (the kit zip with its emulation check), then the kit's workers on every platform: the box's CPU emulation (--check, 96 of 96 vector lanes), the fleet's CUDA 4090 (--check and --bench at 2^24, base nonce 0), the Mac's Metal (proto-metal/packbench.swift --pack) and Apple OpenCL (proto-opencl/host.c --pack --bench-pack), PC 1's RTX 5090 and RX 7600 (tools/class-v5/pc1-v6-bench.ps1), PC 2's Arc B580 (tools/class-v5/arc-v6-bench.ps1); the fingerprint of the 2^24 outputs at base nonce 0 equal on every platform for the all pack and for its known-failed partner, the two distinct` +- Box class: build box + pods + Mac + PC 1 + PC 2 +- Fixtures: F0, F2 +- Cases: + - R2-F03-R02 Same job context produces identical accepted work in node, CPU reference, CUDA, Metal, OpenCL and pool.: partial: the worker half (CPU reference, CUDA, Metal, OpenCL) on the same program packs; the node's and the pool's accepted work on the same job context are the CI steward's and the pool lane's cells; PASS only when every listed platform reads one fingerprint and the node and pool halves are green + ## Automated cases with no harness in the matrix (NOT RUN, the reason) - GOV-02 Approve thresholds before results: the approval is recorded in the registry's approval field; the automated half (thresholds frozen before any run_status) is the gate rule landing by 21:00 diff --git a/docs/plans/igneum-2.0-test-registry.json b/docs/plans/igneum-2.0-test-registry.json index 1e606d5d4..515c64190 100644 --- a/docs/plans/igneum-2.0-test-registry.json +++ b/docs/plans/igneum-2.0-test-registry.json @@ -12105,42 +12105,51 @@ "POW-01", "ROT-02" ], - "updated": "2026-10-08T20:10:24.556Z", + "updated": "2026-10-08T21:18:18.084Z", "evidence_record": { - "reason": "R2-F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map", - "at": "2026-10-08T20:10:24.556Z", - "method": "static", "requirement_id": "R2-F03-R02", "decision": "NOT RUN", - "reviewer": "", - "claim_impact": "", + "method": "GPU", + "cell": "kit:class-v6-fingerprints", + "manifest_sha": "ef0f2ed8", + "run_id": "kit-class-v6-20261008-01", + "evidence": "docs/design/class-v5-stored-state.md; build-1:/srv/artefacts/packs/packs-class-v6-20261008T202808Z.zip; build-1:/srv/builds/_log/v5-class/kits-20261008T202808Z/emu-check.log", + "in_progress": true, + "coverage": "partial: the worker half (CPU reference, CUDA, Metal, OpenCL) on the same program packs; the node's and the pool's accepted work on the same job context are the CI steward's and the pool lane's cells; PASS only when every listed platform reads one fingerprint and the node and pool halves are green", "release_identity": { - "commit": "", + "commit": "ef0f2ed8", "lockfile": "", "binary": "", "network_object": "", "activation": "", "profile_hashes": "" - } + }, + "claim_impact": "", + "reviewer": "", + "at": "2026-10-08T21:18:18.084Z" }, "evidence_records": { - "record": { - "reason": "R2-F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map", - "at": "2026-10-08T20:10:24.556Z", - "method": "static", + "kit:class-v6-fingerprints": { "requirement_id": "R2-F03-R02", "decision": "NOT RUN", - "reviewer": "", - "claim_impact": "", + "method": "GPU", + "cell": "kit:class-v6-fingerprints", + "manifest_sha": "ef0f2ed8", + "run_id": "kit-class-v6-20261008-01", + "evidence": "docs/design/class-v5-stored-state.md; build-1:/srv/artefacts/packs/packs-class-v6-20261008T202808Z.zip; build-1:/srv/builds/_log/v5-class/kits-20261008T202808Z/emu-check.log", + "in_progress": true, + "coverage": "partial: the worker half (CPU reference, CUDA, Metal, OpenCL) on the same program packs; the node's and the pool's accepted work on the same job context are the CI steward's and the pool lane's cells; PASS only when every listed platform reads one fingerprint and the node and pool halves are green", "release_identity": { - "commit": "", + "commit": "ef0f2ed8", "lockfile": "", "binary": "", "network_object": "", "activation": "", "profile_hashes": "" }, - "in_progress": false + "claim_impact": "", + "reviewer": "", + "at": "2026-10-08T21:18:18.084Z" } }, "approvals": { @@ -12148,7 +12157,10 @@ "implementation_complete": null, "evidence_reproduced": null, "claim_authorised": null - } + }, + "run_id": "kit-class-v6-20261008-01", + "evidence_path": "docs/design/class-v5-stored-state.md; build-1:/srv/artefacts/packs/packs-class-v6-20261008T202808Z.zip; build-1:/srv/builds/_log/v5-class/kits-20261008T202808Z/emu-check.log", + "in_progress_since": "2026-10-08T21:18:18.084Z" }, { "id": "R2-F03-R03", diff --git a/igneum-pow/src/accept.rs b/igneum-pow/src/accept.rs index e02c92efe..f96ab824b 100644 --- a/igneum-pow/src/accept.rs +++ b/igneum-pow/src/accept.rs @@ -489,7 +489,11 @@ pub fn is_class_v4_shape(class: &LoadClass) -> bool { // class v5 (docs/design/class-v5-stored-state.md) is judged under the same rules: its state flag is set aside; // class v6 lane 1's index fold and re-weight table are set aside too (the address path and the op table are // not the shape) - && LoadClass { era: None, shadow: None, state: false, fold: false, rw: 0, ..*class } == LoadClass { shadow: None, ..V4_CLASS } + // the window and layer 8 off are set aside too (lane D's finding of 8 October 2026, 19:1x UK: a +nowin or +reg64c + // program was judged by the class v2 parts alone on the chain's path; the harness's own predicate hid it) + // and the era's drawn width (lane D's second finding, 20:0x UK: an era whose width set has more than one width + // writes the drawn width into `mix`, and the rule must judge every width of the family, not the pinned one alone) + && LoadClass { era: None, shadow: None, state: false, fold: false, rw: 0, nowin: false, reg64: false, reg64_chain: false, mix: V4_CLASS.mix, ..*class } == LoadClass { shadow: None, ..V4_CLASS } } /// One pass of the dataflow freshness over the base program then the shadow block (the order of one iteration), @@ -849,6 +853,12 @@ pub fn check_dynamic(p: &Program) -> Result { check_distinct_indices_v4(p)?; } } + // the reg64 window's liveness rule (the window's acceptance tool, wired 8 October 2026, 19:5x UK): keyed on the + // window flag so no other class's verdict moves; the arithmetic-only window is refused at its first dead register, + // the full chain passes (129 one-warp interpretations on the closed form, under a second) + if p.class.reg64 { + check_window_liveness(p)?; + } let half = (ACCEPT_HASHES / 2) as u32; let mut bias_max = 0u32; for (bit, &ones) in acc.bit_ones.iter().enumerate() { diff --git a/igneum-pow/src/bind.rs b/igneum-pow/src/bind.rs index 94edea2bd..dd18b3069 100644 --- a/igneum-pow/src/bind.rs +++ b/igneum-pow/src/bind.rs @@ -91,10 +91,21 @@ pub fn hex(bytes: &[u8]) -> String { /// Bytes from hex (either case). `None` on odd length or a bad digit. pub fn unhex(s: &str) -> Option> { - if s.len() % 2 != 0 { + // on bytes, never on char boundaries (the fuzz of 8 October 2026 found a multi-byte character inside the text + // panicked the slice; a non-ASCII byte is a bad digit) + let b = s.as_bytes(); + if b.len() % 2 != 0 { return None; } - (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect() + let digit = |c: u8| -> Option { + match c { + b'0'..=b'9' => Some(c - b'0'), + b'a'..=b'f' => Some(c - b'a' + 10), + b'A'..=b'F' => Some(c - b'A' + 10), + _ => None, + } + }; + b.chunks(2).map(|c| Some(digit(c[0])? << 4 | digit(c[1])?)).collect() } impl Epoch { diff --git a/igneum-pow/src/emit.rs b/igneum-pow/src/emit.rs index 25f14e983..13ffe7d88 100644 --- a/igneum-pow/src/emit.rs +++ b/igneum-pow/src/emit.rs @@ -255,7 +255,11 @@ fn program_class_header_lines(p: &Program) -> String { return String::new(); } let mut s = String::new(); - if p.program_class() == ProgramClass::V5 { + if p.program_class() == ProgramClass::V6 { + s.push_str("// Program class v6 (the Igneum 2.0 D1 object, docs/design/class-v6-rotating-family.md): generator version 6, class v5's\n"); + s.push_str("// stored-state dataset with the index fold, the re-weight table and the 64-register window (the v6 flags in the class\n"); + s.push_str("// string); a worker that runs another class refuses this pack, and a job line names the class it wants (class=v6 era=).\n"); + } else if p.program_class() == ProgramClass::V5 { s.push_str("// Program class v5 (proof of stored state and of following, docs/design/class-v5-stored-state.md): generator version 5,\n"); s.push_str("// class v4 over a dataset whose every item is keyed by the window's execution state (IGNEUM_STATE_* below, leaves.bin);\n"); s.push_str("// a worker that runs another class refuses this pack, and a job line names the class it wants (class=v5 era=).\n"); @@ -352,7 +356,7 @@ fn class_header_lines(p: &Program) -> String { s.push_str(&format!("#define IGNEUM_LOAD_WIDTH_COUNTS {{ {}, {}, {} }} // loads of 4, 16, 64 bytes per program ", c[0], c[1], c[2])); s.push_str(&format!("#define IGNEUM_BYTES_PER_HASH {} -", p.bytes_per_hash())); +", p.bytes_per_hash_executed())); s.push_str(&format!("#define IGNEUM_FOLD_ROT {FOLD_ROT} ")); s.push_str(&format!("#define IGNEUM_FOLD_MUL {} @@ -503,8 +507,16 @@ fn shadow_block(p: &Program, dialect: CoreDialect) -> String { )); let ty = if dialect == CoreDialect::Cuda { "uint32_t" } else { "uint" }; s.push_str(&format!(" for ({ty} sh = 0u; sh < {reps}u; ++sh) {{\n")); + let prefix = dialect == CoreDialect::Cuda && p.address_mix() && reg64_prefix(); for (k, ins) in p.shadow.iter().enumerate() { + if prefix { + // F05 prefix form: the shadow's writes move S too (the fold reads the live registers) + s.push_str(&format!(" t_ = {};\n", reg64_term(ins.dst as usize))); + } s.push_str(&format!(" {} // s{k} {}\n", shadow_instr_line(dialect, ins), ins.op.name())); + if prefix { + s.push_str(&format!(" S ^= t_ ^ {};\n", reg64_term(ins.dst as usize))); + } } s.push_str(" }\n"); s @@ -1161,13 +1173,60 @@ fn reg_decl(p: &Program, ty: &str) -> String { return format!(" {ty} r0, r1, r2, r3, r4, r5, r6, r7;\n"); } let names: Vec = (0..p.registers()).map(|k| format!("r{k}")).collect(); - let m = if p.address_mix() { format!("\n {ty} m; // reg64 full chain: the address mix of all 64 registers before every load") } else { String::new() }; + let m = if p.address_mix() { + if ty == "uint32_t" && reg64_prefix() { + format!("\n {ty} m, S, p_, t_; // reg64 full chain in the F05 prefix form: S the running xor of the rotated registers, p_ the prefix, t_ the old term") + } else { + format!("\n {ty} m; // reg64 full chain: the address mix of all 64 registers before every load") + } + } else { + String::new() + }; format!(" {ty} {}; // reg64: 64 live registers per lane (two 32-register windows){m}\n", names.join(", ")) } /// reg64 full chain: the mix statement before a load, `m = r0; m = rotl_imm(m, 1u) ^ r1; ... ^ r63;` over the 63 /// registers other than the load's source `src` (the verifier's `addr_src`, the same chain), one line; the same /// text in the three dialects (`rotl_imm` is defined in each). +/// Review B's F05 (8 October 2026): the CUDA texts carry the reg64 address mix in its closed prefix form when this +/// switch is on (`igneum-pow export --reg64-prefix`): `S` is the xor of `a[k] = rotl(r[k], (63 - k) mod 32)` over +/// the 64 registers, kept per lane and updated after every write; a load's source is +/// `r[s] ^ ror(P_s, 1) ^ (S ^ P_s ^ a[s])` with `P_s` the xor of the first `s` terms. The same hash, the same vectors +/// (`verify::reg64_address_source`); a measurement text for the fleet, never the definition. OpenCL and Metal keep +/// the fold. +static REG64_PREFIX: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +pub fn set_reg64_prefix(on: bool) { + REG64_PREFIX.store(on, std::sync::atomic::Ordering::Relaxed); +} + +pub fn reg64_prefix() -> bool { + REG64_PREFIX.load(std::sync::atomic::Ordering::Relaxed) +} + +/// `rotl(rK, (63 - k) mod 32)` as text; a rotation of 0 is the register itself (`rotl_imm` takes 1..31). +fn reg64_term(k: usize) -> String { + let n = (63 - k) % 32; + if n == 0 { format!("r{k}") } else { format!("rotl_imm(r{k}, {n}u)") } +} + +/// The prefix-form mix statement before a load of source `src` (the F05 text): `m = ror(P, 1) ^ S ^ P ^ a[src]`. +fn reg64_prefix_mix_line(src: u8) -> String { + let s = src as usize; + let prefix: Vec = (0..s).map(reg64_term).collect(); + let p = if prefix.is_empty() { "0u".to_string() } else { prefix.join(" ^ ") }; + format!("p_ = {p}; m = rotr_var(p_, 1u) ^ S ^ p_ ^ {};", reg64_term(s)) +} + +/// The prefix form's running total after the register init: `S = a[0] ^ ... ^ a[63]`. +fn reg64_prefix_init(p: &Program) -> String { + if !p.class.reg64 || !p.address_mix() || !reg64_prefix() { + return String::new(); + } + let terms: Vec = (0..p.registers()).map(reg64_term).collect(); + format!(" // F05 prefix form: the running xor of every rotated register\n S = {};\n", terms.join(" ^ ")) +} + fn reg64_mix_line(p: &Program, src: u8) -> String { let mut s = String::new(); for k in 0..p.registers() { @@ -1223,8 +1282,13 @@ fn cuda_instr_lines(p: &Program, geom: DatasetGeom) -> String { let d = format!("r{}", ins.dst); let a = format!("r{}", ins.src); let b = format!("r{}", ins.src2); + let prefix = address_mix && reg64_prefix(); if address_mix && ins.op == Op::Load { - s.push_str(&format!(" {}\n", reg64_mix_line(p, ins.src))); + s.push_str(&format!(" {}\n", if prefix { reg64_prefix_mix_line(ins.src) } else { reg64_mix_line(p, ins.src) })); + } + if prefix { + // the old term of the destination, so S can drop it after the write + s.push_str(&format!(" t_ = {};\n", reg64_term(ins.dst as usize))); } let line = match ins.op { // Metal select(A, B, c) returns c ? B : A, so the true branch is imm2 here as well. @@ -1252,6 +1316,9 @@ fn cuda_instr_lines(p: &Program, geom: DatasetGeom) -> String { Op::Hot => hot_stmt(CoreDialect::Cuda, &d, &a), }; s.push_str(&format!(" {line} // {k} {}\n", ins.op.name())); + if prefix { + s.push_str(&format!(" S ^= t_ ^ {};\n", reg64_term(ins.dst as usize))); + } } s } @@ -1371,6 +1438,7 @@ pub fn cuda_kernel_geom(p: &Program, memhard: Option<&MixParams>, geom: DatasetG s.push_str(&init_line(p, "uint32_t", i)); } s.push_str(®64_init(p)); + s.push_str(®64_prefix_init(p)); s.push_str(&format!("\n for (uint32_t it = 0u; it < {ITERATIONS}u; ++it) {{\n uint32_t sel = r0;\n")); s.push_str(&cuda_instr_lines(p, geom)); s.push_str(&shadow_block(p, CoreDialect::Cuda)); @@ -1536,6 +1604,7 @@ pub fn cuda_kernel_bound_geom(p: &Program, memhard: Option<&MixParams>, geom: Da )); } s.push_str(®64_init(p)); + s.push_str(®64_prefix_init(p)); s.push_str(&format!("\n for (uint32_t it = 0u; it < {ITERATIONS}u; ++it) {{\n uint32_t sel = r0;\n")); s.push_str(&cuda_instr_lines(p, geom)); s.push_str(&shadow_block(p, CoreDialect::Cuda)); @@ -1947,7 +2016,10 @@ pub fn program_header(p: &Program, day: &str, ds: &DatasetSource) -> String { s.push_str("#define IGNEUM_LANES 32\n"); s.push_str(&format!("#define IGNEUM_ITERATIONS {ITERATIONS}\n")); s.push_str(&format!("#define IGNEUM_INSTR_COUNT {INSTR_COUNT}\n")); - s.push_str(&format!("#define IGNEUM_LOADS_PER_HASH {}\n", p.loads_per_hash())); + if p.class.reg64 { + s.push_str("// the executed counts per nonce (review B's V6-02): twice the drawn program's under the 64-register window\n"); + } + s.push_str(&format!("#define IGNEUM_LOADS_PER_HASH {}\n", p.loads_per_hash_executed())); s.push_str(&format!("#define IGNEUM_WIDE_LOADS_PER_HASH {}\n", p.wide_loads_per_hash())); s.push_str(&format!("#define IGNEUM_OP_MIX {}\n", jstr(&p.op_mix()))); s.push_str(&program_class_header_lines(p)); @@ -2189,7 +2261,7 @@ pub fn program_json(p: &Program, day: &str, ds: &DatasetSource) -> String { s.push_str(" \"registers\": 8,\n"); s.push_str(&format!(" \"iterations\": {ITERATIONS},\n")); s.push_str(&format!(" \"instruction_count\": {INSTR_COUNT},\n")); - s.push_str(&format!(" \"loads_per_hash\": {},\n", p.loads_per_hash())); + s.push_str(&format!(" \"loads_per_hash\": {},\n", p.loads_per_hash_executed())); if p.program_class() != ProgramClass::V2 { s.push_str(&format!(" \"program_class\": {},\n", jstr(p.program_class().name()))); if p.program_class() == ProgramClass::V4 { @@ -2239,7 +2311,7 @@ pub fn program_json(p: &Program, day: &str, ds: &DatasetSource) -> String { s.push_str(&format!(" \"load_slots\": {},\n", p.class.load_slots)); s.push_str(&format!(" \"load_mix_percent_4_16_64\": [{}, {}, {}],\n", p.class.mix[0], p.class.mix[1], p.class.mix[2])); s.push_str(&format!(" \"load_width_counts_4_16_64\": [{}, {}, {}],\n", c[0], c[1], c[2])); - s.push_str(&format!(" \"bytes_per_hash\": {},\n", p.bytes_per_hash())); + s.push_str(&format!(" \"bytes_per_hash\": {},\n", p.bytes_per_hash_executed())); if p.has_scratch() { s.push_str(&format!(" \"scratch_ops_per_hash\": {},\n", p.scratch_ops_per_hash())); s.push_str(&format!(" \"scratch_kib_per_warp\": {},\n", p.class.scratch_kb)); @@ -2610,19 +2682,42 @@ pub fn export_pack(epoch: &Epoch, day: &str, source: &str) -> Pack { v.hot_fnv = h.fnv1a64(); } let is_mh = memhard.is_some(); - let mut files = vec![ - ("program.json".to_string(), program_json(p, day, ds)), - ("vectors.json".to_string(), vectors_json_geom(p, day, geom, &bases, &outs, &v, source, is_mh)), + let texts: Vec<(String, String)> = vec![ ("kernel.cu".to_string(), cuda_kernel_geom(p, memhard, geom)), ("kernel.cl".to_string(), opencl_kernel_geom(p, memhard, geom)), - ("program.h".to_string(), program_header(p, day, ds)), - ("vectors.h".to_string(), vectors_header(p, &bases, &outs, &v, mask, source, is_mh)), ("program.metal".to_string(), metal_program_geom(p, geom, LoadSource::Stored)), // Header-bound kernels (3 October 2026, bind.rs): new files, the seven above are unchanged. ("program_bound.metal".to_string(), metal_program_bound_geom(p, geom)), ("kernel_bound.cu".to_string(), cuda_kernel_bound_geom(p, memhard, geom)), ("kernel_bound.cl".to_string(), opencl_kernel_bound_geom(p, memhard, geom)), ]; + // A06 (the external review of 8 October 2026): the pack authenticates its kernel texts by hash, not by metadata: + // identity.json carries the program id, the dataset and era identities and the BLAKE2b-256 of every kernel file. + // A file of its own, so every pinned pack's twelve files stay byte for byte (the readers ignore it). + let hashes: Vec = texts.iter().map(|(n, t)| format!(" {}: \"{}\"", jstr(n), hex_bytes(&crate::blake2b::blake2b_256(&[t.as_bytes()])))).collect(); + let identity = format!( + "{{\n \"program_id\": \"{:#018x}\",\n \"load_class\": {},\n \"generator\": {},\n \"day\": {},\n \"dataset_bytes\": {},\n \"kernel_blake2b256\": {{\n{}\n }},\n \"rule\": \"the external review's A06 (8 October 2026): a pack's program, dataset and work identities are distinct, and its kernel texts are authenticated by the hash of their bytes, never by the metadata beside them\"\n}}\n", + p.program_id(), + jstr(&p.class.name()), + p.generator, + jstr(day), + (ds.geom.words as u128) * 4, + hashes.join(",\n") + ); + // the pack's file order as the pinned packs carry it, identity.json second + let mut texts = texts.into_iter(); + let kernel_cu = texts.next().unwrap(); + let kernel_cl = texts.next().unwrap(); + let mut files = vec![ + ("program.json".to_string(), program_json(p, day, ds)), + ("identity.json".to_string(), identity), + ("vectors.json".to_string(), vectors_json_geom(p, day, geom, &bases, &outs, &v, source, is_mh)), + kernel_cu, + kernel_cl, + ("program.h".to_string(), program_header(p, day, ds)), + ("vectors.h".to_string(), vectors_header(p, &bases, &outs, &v, mask, source, is_mh)), + ]; + files.extend(texts); if let Some(mp) = memhard { files.push(("memhard.h".to_string(), cuda_memhard_header(p, mp))); files.push(("memhard.metal".to_string(), metal_memhard_for(p, mp))); diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs index 61fe4874b..ce0084681 100644 --- a/igneum-pow/src/generator.rs +++ b/igneum-pow/src/generator.rs @@ -255,6 +255,11 @@ pub struct LoadClass { /// optimiser split [`NONLOAD_WEIGHTS_RW`] (sum 83, `or` never drawn); 2 the census lane's neighbouring table /// [`NONLOAD_WEIGHTS_RW2`] (sum 75). The draw rolls against the table's own sum ([`LoadClass::nonload_weights`]). pub rw: u8, + /// D2 experiment "layer 8 off" (the coordinator's order, 8 October 2026, 18:12 UK; a research class behind `+nowin`): + /// the era layout's window-layer draw is removed, every load site reads the whole dataset (`win = 0`, `off = 0`). + /// The program stream still consumes the two window draws per instruction, so the base program's instructions are + /// the class's without the flag; only the windows move. `false` for every other class. + pub nowin: bool, /// The 64-register window (the hash lane's reg64 measurement, 8 October 2026, a research class behind `+reg64` /// and `--reg64`): each lane holds 64 live 32-bit registers. r0..r7 are seeded as today, r8..r63 derived from them /// (`r[k] = r[k & 7] * 0x9E3779B9 + k`); the 64 drawn instructions run twice per iteration in an interleaved @@ -455,13 +460,13 @@ impl LoadClass { impl LoadClass { /// Generator version 2 as adopted on 4 October 2026: 16 loads of one word. The lottery hash. pub const V2: LoadClass = - LoadClass { mix: [100, 0, 0], load_slots: LOAD_SLOTS as u8, scratch: None, scratch_kb: 0, mixer_mult: 1, growth: false, era: None, hot: None, derive_len: 0, shadow: None, state: false, wide8: false, fold: false, rw: 0, reg64: false, reg64_chain: false }; + LoadClass { mix: [100, 0, 0], load_slots: LOAD_SLOTS as u8, scratch: None, scratch_kb: 0, mixer_mult: 1, growth: false, era: None, hot: None, derive_len: 0, shadow: None, state: false, wide8: false, fold: false, rw: 0, nowin: false, reg64: false, reg64_chain: false }; /// The construction decided for program class v3 on 5 October 2026 (Counter ASIC 2.0, `docs/plans/mixer-x4.md`): /// version 2 loads (16 slots of one word, no scratch, no width roll, so the program stream is version 2's), the /// mixer applied 4 times per round, and the cache growth rule. Name "mx4". pub const MX4: LoadClass = - LoadClass { mix: [100, 0, 0], load_slots: LOAD_SLOTS as u8, scratch: None, scratch_kb: 0, mixer_mult: 4, growth: true, era: None, hot: None, derive_len: 0, shadow: None, state: false, wide8: false, fold: false, rw: 0, reg64: false, reg64_chain: false }; + LoadClass { mix: [100, 0, 0], load_slots: LOAD_SLOTS as u8, scratch: None, scratch_kb: 0, mixer_mult: 4, growth: true, era: None, hot: None, derive_len: 0, shadow: None, state: false, wide8: false, fold: false, rw: 0, nowin: false, reg64: false, reg64_chain: false }; /// The era class over `base` (`docs/plans/era-layout.md`): the parameters drawn by [`era_draw`]; when `allowed` /// has more than one width the drawn width becomes the class mix (every load that width), otherwise the base @@ -634,6 +639,21 @@ impl LoadClass { LoadClass { rw, ..self } } + /// D2 "layer 8 off": the class with the window-layer draw removed (every load site reads the whole dataset). + pub fn with_nowin(self) -> LoadClass { + LoadClass { nowin: true, ..self } + } + + /// The class v6 object's draw rules (the external review of 8 October 2026, A02 and A08, fixed by construction for + /// every class carrying a v6 flag: the index fold, a re-weight table, the register window or layer 8 off): five + /// of the non-load slots are shuffles whose masks are the five lane dimensions 1, 2, 4, 8, 16 in a drawn order, so + /// every accepted program mixes all 32 lanes by construction; and a `mad` never names its destination as its + /// second source (`d = a * d + d` is `d * (a + 1)`, not a bijection in `d` when `a` is odd). Every other class + /// draws as before. + pub fn is_v6(&self) -> bool { + self.fold || self.rw != 0 || self.reg64 || self.nowin + } + /// The non-load op table this class draws from, in draw order, and its sum (the roll's range). The plain table /// for every class without the re-weight flag, so their streams are byte for byte what they were. pub fn nonload_weights(&self) -> (&'static [(Op, u64); 10], u64) { @@ -677,6 +697,10 @@ impl LoadClass { pub fn parse(s: &str) -> Option { // class v6 lane 1: "+fold" (the index fold) and "+rw" / "+rw2" (the re-weight table) over // any class, in any order, outermost of all + // D2 "layer 8 off": "+nowin" over any class, in any order with the other suffixes + if let Some(base) = s.strip_suffix("+nowin") { + return Some(LoadClass::parse(base)?.with_nowin()); + } if let Some(base) = s.strip_suffix("+fold") { return Some(LoadClass::parse(base)?.with_fold()); } @@ -829,6 +853,10 @@ impl LoadClass { if self.fold { return format!("{}+fold", LoadClass { fold: false, ..*self }.name()); } + if self.nowin { + // D2 "layer 8 off": "+nowin" sits inside "+fold" and "+rw" and outside "+reg64" and "+state" + return format!("{}+nowin", LoadClass { nowin: false, ..*self }.name()); + } if self.reg64 { // "+reg64" / "+reg64c": the 64-register window is a suffix on any class, outermost let base = LoadClass { reg64: false, reg64_chain: false, ..*self }.name(); @@ -935,6 +963,10 @@ pub const GENERATOR_VERSION_V4: u32 = 4; /// Generator version of a class v5 program (proof of stored state and of following, 7 October 2026, PROPOSED: /// `program_id(5, seed, attempt)`; `docs/design/class-v5-stored-state.md`). pub const GENERATOR_VERSION_V5: u32 = 5; +/// Class v6 (the Igneum 2.0 D1 object, 8 October 2026; review B's F03): the class v5 draw with the v6 rules (the index fold, the +/// re-weight table, the 64-register window with the full chain, the five shuffle dimensions and the mad operand rule by +/// construction, layer 8 off when the flag says so), generator 6. +pub const GENERATOR_VERSION_V6: u32 = 6; /// The program class of an epoch (Counter ASIC 2.0, 5 October 2026, `docs/plans/counter-asic-2-rollout.md`): one /// height switch in the node, `program_class_v3_activation_daa`, rounded up to an epoch boundary, decides which @@ -951,6 +983,8 @@ pub enum ProgramClass { /// Class v5 (`docs/design/class-v5-stored-state.md`, behind `program_class_v5_activation_daa`): class v4's program /// over a dataset whose every item is keyed by the window's execution state ([`V5_CLASS`]), generator 5. V5, + /// Class v6 (the Igneum 2.0 D1 object; review B's F03, 8 October 2026): [`V6_CLASS`], generator 6. + V6, } /// The load class of program class v3, decided 5 October 2026 (Counter ASIC 2.0, `docs/plans/counter-asic-2-status.md` @@ -973,6 +1007,9 @@ pub const V4_CLASS: LoadClass = LoadClass { shadow: Some(ShadowClass { instrs: V /// program draw, the shadow block, the era draw and the ladder rung are class v4's, draw for draw; only the item /// derivation and the program id change. pub const V5_CLASS: LoadClass = LoadClass { state: true, ..V4_CLASS }; +/// The class v6 object ("mx8+sh256x27+state+reg64c+fold+rw"): class v5 with the index fold, the k lane's re-weight table +/// and the 64-register window with the full chain; layer 8 off is the fifth flag, drawn by the chain's family flags. +pub const V6_CLASS: LoadClass = LoadClass { fold: true, rw: 1, reg64: true, reg64_chain: true, ..V5_CLASS }; /// The shadow block size of class v4 at every rung of the latency ladder (`docs/design/latency-ladder.md`): 256 /// instructions. The ladder moves the pass count alone. @@ -1037,6 +1074,8 @@ pub fn era_generator_of(base: &LoadClass) -> u32 { match ProgramClass::of_load_class(base) { Some(ProgramClass::V4) => GENERATOR_VERSION_V4, Some(ProgramClass::V5) => GENERATOR_VERSION_V5, + Some(ProgramClass::V6) => GENERATOR_VERSION_V6, + _ if base.is_v6() => GENERATOR_VERSION_V6, _ if base.state => GENERATOR_VERSION_V5, _ => GENERATOR_VERSION_V3, } @@ -1067,6 +1106,7 @@ impl ProgramClass { ProgramClass::V3 => V3_CLASS, ProgramClass::V4 => V4_CLASS, ProgramClass::V5 => V5_CLASS, + ProgramClass::V6 => V6_CLASS, } } @@ -1077,12 +1117,13 @@ impl ProgramClass { ProgramClass::V3 => GENERATOR_VERSION_V3, ProgramClass::V4 => GENERATOR_VERSION_V4, ProgramClass::V5 => GENERATOR_VERSION_V5, + ProgramClass::V6 => GENERATOR_VERSION_V6, } } /// Whether the class's dataset is keyed by the window's execution state (class v5). pub fn has_state(&self) -> bool { - *self == ProgramClass::V5 + matches!(self, ProgramClass::V5 | ProgramClass::V6) } /// The class of a generator version: 2, 3 and 4 are the three classes, anything else is no class this crate runs. @@ -1092,6 +1133,7 @@ impl ProgramClass { GENERATOR_VERSION_V3 => Some(ProgramClass::V3), GENERATOR_VERSION_V4 => Some(ProgramClass::V4), GENERATOR_VERSION_V5 => Some(ProgramClass::V5), + GENERATOR_VERSION_V6 => Some(ProgramClass::V6), _ => None, } } @@ -1103,6 +1145,7 @@ impl ProgramClass { ProgramClass::V3 => "v3", ProgramClass::V4 => "v4", ProgramClass::V5 => "v5", + ProgramClass::V6 => "v6", } } @@ -1112,6 +1155,7 @@ impl ProgramClass { "v3" => Some(ProgramClass::V3), "v4" => Some(ProgramClass::V4), "v5" => Some(ProgramClass::V5), + "v6" => Some(ProgramClass::V6), _ => None, } } @@ -1125,6 +1169,7 @@ impl ProgramClass { /// is v3, [`V4_CLASS`] is v4; a measurement class (a width, a derivation length, another shadow size) is none. pub fn of_load_class(class: &LoadClass) -> Option { let base = LoadClass { era: None, reg64: false, reg64_chain: false, ..*class }; + let v6base = LoadClass { era: None, nowin: false, ..*class }; if base == LoadClass::V2 { Some(ProgramClass::V2) } else if base == V3_CLASS { @@ -1133,6 +1178,8 @@ impl ProgramClass { Some(ProgramClass::V4) } else if base == V5_CLASS { Some(ProgramClass::V5) + } else if v6base == V6_CLASS { + Some(ProgramClass::V6) } else { None } @@ -1194,9 +1241,25 @@ impl Program { } out } + /// The drawn program's memory operations per nonce (16 load slots times the eight iterations on every class): + /// the count the acceptance rule and the draw's bookkeeping read (the acceptance judges the drawn program's + /// sites). The hash EXECUTES [`Program::loads_per_hash_executed`] of them, twice this under the 64-register + /// window; the served figures and the pack's headers name the executed count (review B's V6-02). pub fn loads_per_hash(&self) -> usize { self.instrs.iter().filter(|i| i.op.is_load()).count() * ITERATIONS } + + /// Memory operations the hash EXECUTES per nonce (review B's V6-02, 8 October 2026): the scheduled statements + /// (the drawn program's loads, twice under the 64-register window) times the eight iterations; asserted to + /// agree with the drawn count except by the window's factor of two. The emitters alone read it (program.h, + /// program.json): the acceptance reads the drawn count, so no verdict moves with the served figure (the first + /// form of this change, 0266c9ec0, routed the executed count into the acceptance and moved every reg64 verdict). + pub fn loads_per_hash_executed(&self) -> usize { + let drawn = self.loads_per_hash(); + let executed = self.scheduled().iter().filter(|i| i.op.is_load()).count() * ITERATIONS; + assert!(executed == drawn || (self.class.reg64 && executed == 2 * drawn), "the executed load count {executed} disagrees with the drawn {drawn}"); + executed + } pub fn wide_loads_per_hash(&self) -> usize { self.instrs.iter().filter(|i| i.op == Op::WLoad).count() * ITERATIONS } @@ -1204,9 +1267,21 @@ impl Program { self.instrs.iter().any(|i| i.op == Op::WLoad) } /// Dataset bytes read per hash: 4 per one-word load, 16 and 64 for the wider loads of the experiment. + /// Dataset bytes the drawn program's loads demand per nonce (the acceptance's and the draw's figure). pub fn bytes_per_hash(&self) -> usize { self.instrs.iter().filter(|i| i.op == Op::Load).map(|i| i.width as usize * 4).sum::() * ITERATIONS } + + /// Dataset bytes the hash's EXECUTED loads demand per nonce (review B's V6-02): the scheduled loads' widths in + /// words times 4, times the eight iterations; a demand figure, not the memory's transactions (a 4-byte load is + /// a 32-byte sector on NVIDIA and a 64-byte line on AMD; the served text names the model it quotes). The + /// emitters alone read it. + pub fn bytes_per_hash_executed(&self) -> usize { + let drawn = self.bytes_per_hash(); + let executed = self.scheduled().iter().filter(|i| i.op == Op::Load).map(|i| i.width as usize * 4).sum::() * ITERATIONS; + assert!(executed == drawn || (self.class.reg64 && executed == 2 * drawn), "the executed byte demand {executed} disagrees with the drawn {drawn}"); + executed + } /// Scratch read-modify-writes per hash (variant 5): each reads 16 bytes and writes 16 bytes. pub fn scratch_ops_per_hash(&self) -> usize { self.instrs.iter().filter(|i| i.op == Op::Scratch).count() * ITERATIONS @@ -1328,6 +1403,7 @@ impl Program { GENERATOR_VERSION_V3 => ProgramClass::V3, GENERATOR_VERSION_V4 => ProgramClass::V4, GENERATOR_VERSION_V5 => ProgramClass::V5, + GENERATOR_VERSION_V6 => ProgramClass::V6, _ => ProgramClass::V2, } } @@ -1458,6 +1534,14 @@ pub fn program_id_class_recipe(generator: u32, seed: &[u32; 8], attempt: u32, cl // class v6 lane 1: the index fold moves every era load address r.lit(b"fold/"); } + if class.nowin { + // D2 "layer 8 off": the window layer is part of the construction + r.lit(b"nowin/"); + } + if class.is_v6() { + // the v6 draw rules (A02 five shuffle dimensions, A08 the mad operand rule) are part of the construction + r.lit(b"v6draw/"); + } if class.rw != 0 { // class v6 lane 1: the re-weight table moves the op draw r.lit(b"rw/"); @@ -1626,6 +1710,23 @@ pub fn candidate_from_words_class( for &slot in &p[..slots] { is_load[slot as usize] = true; } + // class v6 (A02, by construction): five of the non-load slots are shuffles over the five lane dimensions in a + // drawn order; drawn from the stream after the load slots, so no other class's stream moves + let mut is_shfl = [false; INSTR_COUNT]; + let mut shfl_masks = [1u8, 2, 4, 8, 16]; + if class.is_v6() { + let rest = &mut p[slots..]; + for i in 0..5 { + let j = i + rng.below((rest.len() - i) as u64) as usize; + rest.swap(i, j); + is_shfl[rest[i] as usize] = true; + } + for i in 0..5 { + let j = i + rng.below((5 - i) as u64) as usize; + shfl_masks.swap(i, j); + } + } + let mut shfl_next = 0usize; // Variant 5: the first k drawn load slots (a uniform k-subset, the draw order is random) are scratch ops. let mut is_scratch = [false; INSTR_COUNT]; for &slot in &p[..class.scratch_slots()] { @@ -1652,7 +1753,7 @@ pub fn candidate_from_words_class( // class v6 lane 1: the index fold and the re-weight table are set aside too (the address path and the table are not // the shape; a +fold or +rw program draws its sources under the same rule) let source_rule_v4 = matches!(class.shadow, Some(ShadowClass { instrs: V4_SHADOW_INSTRS, .. })) - && LoadClass { era: None, shadow: None, state: false, fold: false, rw: 0, ..class } == LoadClass { shadow: None, ..V4_CLASS }; + && LoadClass { era: None, shadow: None, state: false, fold: false, rw: 0, nowin: false, reg64: false, reg64_chain: false, mix: V4_CLASS.mix, ..class } == LoadClass { shadow: None, ..V4_CLASS }; // the op table and the roll's range: the plain table at 75 for every class without the re-weight flag let (weights, weights_sum) = class.nonload_weights(); let mut fresh = [false; 8]; @@ -1682,6 +1783,9 @@ pub fn candidate_from_words_class( Op::Load }; } + if is_shfl[k] { + op = Op::Shfl; + } let dst = rng.below(8); let src = if op.is_load() { let mut eligible = [0u64; 8]; @@ -1710,12 +1814,21 @@ pub fn candidate_from_words_class( a } }; - let b = rng.below(8); + let mut b = rng.below(8); + // class v6 (A08, by construction): a mad's second source is never its destination + if class.is_v6() && op == Op::Mad && b == dst { + b = (b + 1) & 7; + } let imm = rng.next() as u32; let imm2 = rng.next() as u32; let rot = 1 + rng.below(31) as u32; let bit = rng.below(32); - let mask = 1u8 << rng.below(5); + let mut mask = 1u8 << rng.below(5); + // class v6 (A02): the reserved shuffle slots take the five dimensions in the drawn order + if is_shfl[k] { + mask = shfl_masks[shfl_next]; + shfl_next += 1; + } // Version 2 loads take no width roll, so a mixer class with version 2 loads draws the version 2 program let width = if class.takes_width_roll() { class.width_for_roll(rng.below(100)) } else { 1 }; let width = if op == Op::Load { width } else { 1 }; @@ -1723,7 +1836,8 @@ pub fn candidate_from_words_class( let (win, off) = if class.era.is_some() { let k = rng.below(3) as u8; let o = (rng.next() as u32 & ((1u32 << k) - 1)) as u8; - if op == Op::Load { + // D2 "layer 8 off": the draws are consumed (the stream is the class's) and every site reads the whole dataset + if op == Op::Load && !class.nowin { (k, o) } else { (0, 0) @@ -1963,6 +2077,7 @@ pub fn generate_from_seed_bytes_program_class(seed_string: &str, seed_bytes: &[u (ProgramClass::V3, Some(era)) => return generate_era(seed_string, seed_bytes, V3_CLASS, era, &V3_ALLOWED), (ProgramClass::V4, Some(era)) => return generate_era_generator(seed_string, seed_bytes, V4_CLASS, era, &V3_ALLOWED, GENERATOR_VERSION_V4), // Class v5: the same draw inside V5_CLASS (V4_CLASS plus the state flag, which the draw does not read), generator 5. + (ProgramClass::V6, Some(era)) => return generate_era_generator(seed_string, seed_bytes, V6_CLASS, era, &V3_ALLOWED, GENERATOR_VERSION_V6), (ProgramClass::V5, Some(era)) => return generate_era_generator(seed_string, seed_bytes, V5_CLASS, era, &V3_ALLOWED, GENERATOR_VERSION_V5), _ => {} } @@ -1980,11 +2095,11 @@ pub fn generate_from_seed_bytes_program_class(seed_string: &str, seed_bytes: &[u /// and class v4 at rung 0, is [`generate_from_seed_bytes_program_class`] byte for byte. The base program, the 16 loads /// and the era draw do not move with the rung: only the pass count of the shadow block does. pub fn generate_from_seed_bytes_program_class_shadow(seed_string: &str, seed_bytes: &[u8], class: ProgramClass, era_bytes: Option<&[u8]>, shadow_reps: u16) -> Program { - if !matches!(class, ProgramClass::V4 | ProgramClass::V5) || shadow_reps == 0 || shadow_reps == V4_SHADOW_REPS { + if !matches!(class, ProgramClass::V4 | ProgramClass::V5 | ProgramClass::V6) || shadow_reps == 0 || shadow_reps == V4_SHADOW_REPS { return generate_from_seed_bytes_program_class(seed_string, seed_bytes, class, era_bytes); } // class v5 at a rung: class v4's rung with the state flag, generator 5 - let (base, generator) = if class == ProgramClass::V5 { (v5_class_at(shadow_reps), GENERATOR_VERSION_V5) } else { (v4_class_at(shadow_reps), GENERATOR_VERSION_V4) }; + let (base, generator) = if class == ProgramClass::V6 { (LoadClass { fold: true, rw: 1, reg64: true, reg64_chain: true, ..v5_class_at(shadow_reps) }, GENERATOR_VERSION_V6) } else if class == ProgramClass::V5 { (v5_class_at(shadow_reps), GENERATOR_VERSION_V5) } else { (v4_class_at(shadow_reps), GENERATOR_VERSION_V4) }; match era_bytes { Some(era) => generate_era_generator(seed_string, seed_bytes, base, era, &V3_ALLOWED, generator), None => { @@ -2330,14 +2445,15 @@ mod tests { assert_eq!(v3.program_id(), program_id(GENERATOR_VERSION_V3, &v3.seed, v3.attempt)); assert_ne!(v3.program_id(), program_id(GENERATOR_VERSION, &v3.seed, v3.attempt)); assert_ne!(v3.program_id(), v2.program_id()); - for c in [ProgramClass::V2, ProgramClass::V3, ProgramClass::V4, ProgramClass::V5] { + for c in [ProgramClass::V2, ProgramClass::V3, ProgramClass::V4, ProgramClass::V5, ProgramClass::V6] { assert_eq!(ProgramClass::parse(c.name()), Some(c)); assert_eq!(ProgramClass::from_generator(c.generator_version()), Some(c)); assert_eq!(ProgramClass::from_u8(c.as_u8()), Some(c)); } assert_eq!(ProgramClass::from_generator(1), None); - assert_eq!(ProgramClass::from_generator(6), None); - assert_eq!(ProgramClass::parse("v6"), None); + assert_eq!(ProgramClass::from_generator(6), Some(ProgramClass::V6), "class v6 is generator 6 (review B's F03, 8 October 2026)"); + assert_eq!(ProgramClass::from_generator(7), None); + assert_eq!(ProgramClass::parse("v7"), None); assert_eq!(ProgramClass::default(), ProgramClass::V2); assert_eq!(ProgramClass::V2.load_class(), LoadClass::V2); assert!(!ProgramClass::V2.has_era() && ProgramClass::V3.has_era() && ProgramClass::V4.has_era() && ProgramClass::V5.has_era()); diff --git a/igneum-pow/src/main.rs b/igneum-pow/src/main.rs index 393b1eb81..205f6ce66 100644 --- a/igneum-pow/src/main.rs +++ b/igneum-pow/src/main.rs @@ -122,7 +122,8 @@ fn usage() -> ! { \x20 --era-widths 4[,16,32,64] the width set the era draws from, in bytes (default 4: pinned; more lets the era draw it; 32 only with the w32 class)\n\ \x20 --dataset-words N research class ds55: the dataset at N words (a multiple of 65,536 in 2^28 ..= 2^31; 1476395008 = 5.5 GiB); a non-power-of-two uses idx = (src * N) >> 32 in every load (spec 01 section 1.13.3), a power of two is --dataset-log2\n\\ \x20 --reg64 the 64-register window per lane over the class (research; the same as --class +reg64; CUDA, OpenCL and Metal texts)\n\ - \x20 --reg64-chain reg64 with the full-chain address mix: every load's address consumes all 64 registers (the same as --class +reg64c)" + \x20 --reg64-chain reg64 with the full-chain address mix: every load's address consumes all 64 registers (the same as --class +reg64c)\n\ + \x20 --reg64-prefix export: the CUDA texts carry the reg64 chain in its closed prefix form (review B's F05; the same hash and vectors, a measurement text)" ); std::process::exit(2) } @@ -189,6 +190,7 @@ fn parse() -> Args { a.reg64 = true; a.reg64_chain = true; } + "--reg64-prefix" => igneum_pow::emit::set_reg64_prefix(true), _ => usage(), } } @@ -229,10 +231,29 @@ fn main() { // gate G2 (5 October 2026, ported from branch ca2-era for the class v4 gate run): `--count N` prints // "nonce hash" for N consecutive 64-bit nonces from --nonce, one epoch build, to re-hash a worker's // found lines (the same prehash, target ff..ff) - for k in 0..a.count { - let n = a.nonce.wrapping_add(k); - println!("{n} {:016x}", e.hash_bound(&prehash, n)); + // one warp per 32 nonces (the P01 campaign, 8 October 2026: a million nonces per pack in minutes, not + // hours; the per-nonce form hashed the whole aligned group for every nonce) + let end = a.nonce.wrapping_add(a.count); + let mut n = a.nonce; + let mut out = String::with_capacity(1 << 16); + use std::io::Write; + let stdout = std::io::stdout(); + let mut lock = stdout.lock(); + while n != end { + let base = n & !31; + let warp = e.hash_warp_bound(&prehash, base); + let mut m = n; + while m != end && (m & !31) == base { + out.push_str(&format!("{m} {:016x}\n", warp[(m & 31) as usize])); + m = m.wrapping_add(1); + } + n = m; + if out.len() > (1 << 15) { + lock.write_all(out.as_bytes()).unwrap(); + out.clear(); + } } + lock.write_all(out.as_bytes()).unwrap(); return; } let init = igneum_pow::bind::block_init_words(&prehash, a.nonce); diff --git a/igneum-pow/src/packcheck.rs b/igneum-pow/src/packcheck.rs index a7cbea7f5..b75e5a073 100644 --- a/igneum-pow/src/packcheck.rs +++ b/igneum-pow/src/packcheck.rs @@ -199,14 +199,14 @@ pub fn verify_pack_texts_chain( // carries IGNEUM_STATE_ROOT_HEX (and the shadow block of v4) and no other generator does. let state_root_hex = define_str(program_h, "IGNEUM_STATE_ROOT_HEX"); match (class, state_root_hex.is_some()) { - (ProgramClass::V5, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 5 (class v5) without IGNEUM_STATE_ROOT_HEX: not a class v5 pack".into())), - (ProgramClass::V5, true) => {} + (ProgramClass::V5 | ProgramClass::V6, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 5 or 6 (a state class) without IGNEUM_STATE_ROOT_HEX: not a class v5 pack".into())), + (ProgramClass::V5 | ProgramClass::V6, true) => {} (_, true) => return Err(PackFault::Disagree(format!("IGNEUM_GENERATOR {generator} with class v5 state lines: a program over state leaves is generator 5 (export the pack as class v5)"))), _ => {} } match (class, shadow > 0) { (ProgramClass::V4, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack".into())), - (ProgramClass::V5, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack".into())), + (ProgramClass::V5 | ProgramClass::V6, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 5 or 6 (a state class) without IGNEUM_SHADOW_INSTRS: not a class v5 pack".into())), // the class v4 stream sub-version (AP-F8-1 amendment, 7 October 2026): a generator 4 pack from before the // load-source rule carries no IGNEUM_PROGRAM_SUBVERSION and its program id is another stream's; refused (ProgramClass::V4, true) if define_u32(program_h, "IGNEUM_PROGRAM_SUBVERSION") != Some(u32::from(crate::generator::PROGRAM_SUBVERSION_V4)) => { diff --git a/igneum-pow/src/verify.rs b/igneum-pow/src/verify.rs index 75ea927b1..c0dc217b8 100644 --- a/igneum-pow/src/verify.rs +++ b/igneum-pow/src/verify.rs @@ -1172,3 +1172,91 @@ mod tests { assert!(!e.verify_block(0, w[0] - 1)); } } + + +/// Review B's F05 (8 October 2026): the reg64 full-chain address source in closed form. The reference fold +/// (`m = r[k0]; m = rotl(m, 1) ^ r[k1]; ...` over the 63 registers other than `s`, in index order) equals +/// `r[s] ^ ror(P_s, 1) ^ (S ^ P_s ^ a[s])` with `a[k] = rotl(r[k], (63 - k) mod 32)`, `S` the xor of every `a[k]` +/// and `P_s` the xor of `a[k]` for `k < s`: a prefix-xor structure answers every load from one `S` and one prefix, +/// which is the incremental form a chip would keep. The verifier keeps the fold as the definition; this form is the +/// test's and the fleet's measurement's. +pub fn reg64_address_source(r: &[u32; 64], s: usize) -> u32 { + let a = |k: usize| r[k].rotate_left(((63 - k) % 32) as u32); + let mut total = 0u32; + let mut prefix = 0u32; + for k in 0..64 { + total ^= a(k); + if k < s { + prefix ^= a(k); + } + } + r[s] ^ prefix.rotate_right(1) ^ (total ^ prefix ^ a(s)) +} + +/// The reference fold of [`reg64_address_source`] on one lane's registers, as `interpret_warp_core` computes it. +pub fn reg64_address_source_reference(r: &[u32; 64], s: usize) -> u32 { + let mut m = 0u32; + let mut started = false; + for (k, &v) in r.iter().enumerate() { + if k == s { + continue; + } + m = if started { m.rotate_left(1) ^ v } else { v }; + started = true; + } + r[s] ^ m +} + +#[cfg(test)] +mod reg64_mix_tests { + use super::*; + + /// Review B's F05, known-failed first: a form with one rotation off (every term rotated as if it sat after the + /// source) disagrees with the reference on some source; the closed form agrees on every source register over + /// 256 register states drawn from a fixed stream and over the states a real program leaves in its registers. + #[test] + fn reg64_closed_form_equals_the_reference_fold_on_every_source() { + let wrong = |r: &[u32; 64], s: usize| -> u32 { + let a = |k: usize| r[k].rotate_left(((63 - k) % 32) as u32); + let total: u32 = (0..64).map(a).fold(0, |x, y| x ^ y); + r[s] ^ total ^ a(s) + }; + let mut x = 0x9e37_79b9_7f4a_7c15u64; + let mut next = || { + x = x.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut z = x; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + (z ^ (z >> 31)) as u32 + }; + let mut wrong_differs = false; + for _ in 0..256 { + let mut r = [0u32; 64]; + for v in r.iter_mut() { + *v = next(); + } + for s in 0..64 { + assert_eq!(reg64_address_source(&r, s), reg64_address_source_reference(&r, s), "source r{s}"); + if wrong(&r, s) != reg64_address_source_reference(&r, s) { + wrong_differs = true; + } + } + } + assert!(wrong_differs, "the known-failed form must disagree somewhere"); + // the init rule's registers (r0..r7 seeded, r[k] = r[k & 7] * 0x9e3779b9 + k) and a few real updates + let mut r = [0u32; 64]; + for (k, v) in r.iter_mut().enumerate().take(8) { + *v = next() ^ (k as u32); + } + for k in 8..64 { + r[k] = r[k & 7].wrapping_mul(0x9e37_79b9).wrapping_add(k as u32); + } + for step in 0..64 { + let d = (step * 7 + 3) % 64; + r[d] = r[d].wrapping_mul(r[(d + 13) % 64]).wrapping_add(next()); + for s in 0..64 { + assert_eq!(reg64_address_source(&r, s), reg64_address_source_reference(&r, s), "step {step} source r{s}"); + } + } + } +} diff --git a/igneum-pow/tests/ds55.rs b/igneum-pow/tests/ds55.rs index 821cd76cf..4bff40706 100644 --- a/igneum-pow/tests/ds55.rs +++ b/igneum-pow/tests/ds55.rs @@ -68,8 +68,11 @@ fn power_of_two_path_is_byte_identical_to_the_pinned_pack() { assert!(!e.dataset.geom.mulshift); assert_eq!(e.dataset.geom, DatasetGeom::pow2(28)); let out = export_pack(e, &day_label(), SOURCE); - assert_eq!(out.files.len(), 12, "the pack's twelve files"); + assert_eq!(out.files.len(), 13, "the pack's thirteen files"); for (name, text) in &out.files { + if name == "identity.json" { + continue; // A06's identity file is new beside the pinned twelve + } let want = pinned(name); assert!(text == &want, "mx8-devnet-epoch0/{name} differs from the default path's export"); } @@ -219,7 +222,7 @@ fn ds55_pack_fields_and_vectors() { assert_eq!(e.program.seed[0], 0x667d_0fbd); assert_eq!(e.program.seed[1], 0x7b8e_5963); let out = export_pack(e, &day_label(), SOURCE); - assert_eq!(out.files.len(), 12); + assert_eq!(out.files.len(), 13); let pj = &out.files.iter().find(|(n, _)| n == "program.json").unwrap().1; let j: Value = serde_json::from_str(pj).expect("program.json is valid JSON"); assert_eq!(j["program_id"].as_str().unwrap(), "0x73bcbfe8ccf988f1"); diff --git a/igneum-pow/tests/fuzz_parsers.rs b/igneum-pow/tests/fuzz_parsers.rs new file mode 100644 index 000000000..a52ac6ea3 --- /dev/null +++ b/igneum-pow/tests/fuzz_parsers.rs @@ -0,0 +1,132 @@ +//! POW-01 and POW-02's malformed-input half (the Test and Acceptance Standard, 8 October 2026): 10,000 mutated inputs +//! per parser, every one refused cleanly or accepted, never a panic. The parsers: the class string (`LoadClass::parse`, +//! `ProgramClass::parse`), the hex reader (`bind::unhex`), the IGSD1 state stream (`StateStream::decode`), the era +//! string form (`":<64 hex>"`, the exporter's `parse_era` re-stated here since main.rs keeps it private). The +//! mutations: byte flips, truncation, insertion, duplication and random bytes over a valid seed input, drawn from a +//! fixed SplitMix64 so the run is reproducible; a panic in any parser fails the test (the harness catches none). +use igneum_pow::bind::unhex; +use igneum_pow::generator::{LoadClass, ProgramClass}; +use igneum_pow::state::StateStream; + +struct Rng(u64); +impl Rng { + fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); + z ^ (z >> 31) + } + fn below(&mut self, n: usize) -> usize { + (self.next() % (n.max(1) as u64)) as usize + } +} + +/// One mutation of `seed`: a flipped byte, a truncation, an insertion, a doubled slice, or random bytes of a random length. +fn mutate(rng: &mut Rng, seed: &[u8]) -> Vec { + let mut v = seed.to_vec(); + match rng.below(6) { + 0 => { + if !v.is_empty() { + let i = rng.below(v.len()); + v[i] ^= 1 << rng.below(8); + } + } + 1 => { + let n = rng.below(v.len() + 1); + v.truncate(n); + } + 2 => { + let i = rng.below(v.len() + 1); + v.insert(i, rng.next() as u8); + } + 3 => { + if v.len() > 1 { + let a = rng.below(v.len()); + let b = a + rng.below(v.len() - a); + let slice = v[a..b].to_vec(); + v.splice(a..a, slice); + } + } + 4 => { + let n = rng.below(300); + v = (0..n).map(|_| rng.next() as u8).collect(); + } + _ => { + if !v.is_empty() { + let i = rng.below(v.len()); + v[i] = rng.next() as u8; + } + } + } + v +} + +const N: usize = 10_000; + +#[test] +fn class_strings_never_panic() { + let seeds = ["mx8+sh256x27+state+reg64c+fold+rw", "mx8+sh256x27+state+nowin", "w32m8g-erad810f22d+sh256x27+state", "v2", "mix50-35-15", "scr4k32+hot64k4a", "mx4", "p4,p16,p64", "w64x4", "mx8+rw2"]; + let mut rng = Rng(1); + let (mut some, mut none) = (0, 0); + for k in 0..N { + let s = mutate(&mut rng, seeds[k % seeds.len()].as_bytes()); + let text = String::from_utf8_lossy(&s); + match LoadClass::parse(&text) { + Some(c) => { + // an accepted string names back to something the parser accepts again + assert!(LoadClass::parse(&c.name()).is_some(), "{text:?} -> {} does not re-parse", c.name()); + some += 1; + } + None => none += 1, + } + let _ = ProgramClass::parse(&text); + } + println!("class strings: {some} accepted, {none} refused of {N}"); + assert!(none > N / 2, "the mutations refuse at least half"); +} + +#[test] +fn hex_and_era_strings_never_panic() { + let hex = "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"; + let mut rng = Rng(2); + let mut refused = 0; + for _ in 0..N { + let s = mutate(&mut rng, hex.as_bytes()); + let text = String::from_utf8_lossy(&s); + if unhex(&text).is_none() { + refused += 1; + } + // the era string ":<64 hex>" as the exporter reads it + let era = format!("{}:{}", rng.below(1000), text); + let parsed = era.split_once(':').and_then(|(n, h)| n.parse::().ok().zip(unhex(h))).filter(|(_, b)| b.len() == 32); + if let Some((_, b)) = parsed { + assert_eq!(b.len(), 32); + } + } + println!("hex strings: {refused} refused of {N}"); + assert!(refused > 0); +} + +#[test] +fn state_streams_never_panic() { + // a small valid stream: three records under a fixed root and block + let stream = StateStream { number: 159357, block: [7u8; 32], root: [9u8; 32], records: vec![vec![1, 2, 3], vec![], (0..200u8).collect()] }; + let valid = stream.encode(); + assert_eq!(StateStream::decode(&valid).map(|s| s.records.len()), Ok(3)); + let mut rng = Rng(3); + let (mut ok, mut err) = (0, 0); + for _ in 0..N { + let bytes = mutate(&mut rng, &valid); + match StateStream::decode(&bytes) { + Ok(s) => { + // an accepted stream re-encodes to the bytes it was read from + assert_eq!(s.encode(), bytes, "an accepted stream must round-trip"); + ok += 1; + } + Err(_) => err += 1, + } + } + println!("state streams: {ok} accepted, {err} refused of {N}"); + assert!(err > N / 2); +} diff --git a/igneum-pow/tests/mixer.rs b/igneum-pow/tests/mixer.rs index f4644ae8d..43ddeeb41 100644 --- a/igneum-pow/tests/mixer.rs +++ b/igneum-pow/tests/mixer.rs @@ -347,8 +347,8 @@ fn determinism_v3() { }; println!("determinism {}: two builds equal, against the pinned pack {}", class.name(), dir.display()); for (name, text) in &pa.files { - if name == "vectors.json" || name == "vectors.h" { - continue; // the source string differs ("a" here) + if name == "vectors.json" || name == "vectors.h" || name == "identity.json" { + continue; // the source string differs ("a" here); identity.json is new beside the pinned twelve (A06) } let on_disk = std::fs::read_to_string(dir.join(name)).unwrap(); assert_eq!(&on_disk, text, "{name}"); diff --git a/igneum-pow/tests/packs.rs b/igneum-pow/tests/packs.rs index 9b3aecf48..cd872c6a5 100644 --- a/igneum-pow/tests/packs.rs +++ b/igneum-pow/tests/packs.rs @@ -433,7 +433,9 @@ fn check_export(pack: &str) { if e.dataset.mode() == DatasetMode::MemoryHard { expected.extend(["memhard.h", "memhard.metal"]); } - assert_eq!(out.files.iter().map(|(n, _)| n.as_str()).collect::>(), expected); + let mut with_identity: Vec<&str> = expected.clone(); + with_identity.insert(1, "identity.json"); + assert_eq!(out.files.iter().map(|(n, _)| n.as_str()).collect::>(), with_identity); let mut on_disk: Vec = std::fs::read_dir(pack_dir(pack)) .unwrap() .map(|d| d.unwrap().file_name().to_string_lossy().to_string()) @@ -646,6 +648,9 @@ fn era_emitted_sources_match_and_loads_have_the_era_form() { let source = era_json(pack, "vectors.json")["source"].as_str().unwrap().to_string(); let out = export_pack(e, &day, &source); for (name, text) in &out.files { + if name == "identity.json" { + continue; // A06's identity file is new beside the pinned files + } let want = era_read(pack, name); assert!(text == &want, "{pack}/{name} differs from the emitter"); } @@ -655,7 +660,7 @@ fn era_emitted_sources_match_and_loads_have_the_era_form() { .filter(|n| !n.starts_with('.') && n != "seeds.txt") .collect(); on_disk.sort(); - let mut want: Vec = out.files.iter().map(|(n, _)| n.clone()).collect(); + let mut want: Vec = out.files.iter().map(|(n, _)| n.clone()).filter(|n| n != "identity.json").collect(); want.sort(); assert_eq!(on_disk, want, "{pack}: the pack holds the export's files and seeds.txt only"); let era = e.program.class.era.unwrap(); @@ -869,7 +874,7 @@ fn hot_packs_emitted_sources_and_load_forms() { let file = |name: &str| -> &str { &out.files.iter().find(|(n, _)| n == name).unwrap().1 }; hassert_same_text(pack, "vectors.json", file("vectors.json")); hassert_same_text(pack, "vectors.h", file("vectors.h")); - assert_eq!(out.files.len(), 12); + assert_eq!(out.files.len(), 13); // One form per dialect, exactly 16 - k masked dataset loads and k hot loads in every hash kernel; the fill // kernel is present once per source that builds the table. for (file, load, masked, hot) in [ @@ -974,6 +979,9 @@ fn v5_pack_is_the_v4_program_over_the_state_leaves() { let v = v5_json(pack, "vectors.json"); let out = export_pack(e, v["day"].as_str().unwrap(), v["source"].as_str().unwrap()); for (name, text) in &out.files { + if name == "identity.json" { + continue; // A06's identity file is new beside the pinned files + } assert_eq!(v5_read(pack, name), *text, "{pack}/{name} differs from the export"); } for (name, bytes) in &out.binaries { @@ -1032,8 +1040,11 @@ fn reg64_plain_path_reexports_the_pinned_devnet_pack_unchanged() { assert_eq!(e.program.registers(), 8); assert_eq!(e.program.scheduled(), e.program.instrs, "without the flag the schedule is the drawn program"); let out = export_pack(e, &day_label(pack), PINNED_SOURCE); - assert_eq!(out.files.len(), 12); + assert_eq!(out.files.len(), 13); for (name, text) in &out.files { + if name == "identity.json" { + continue; // A06's identity file is new beside the pinned twelve + } assert_same_text(pack, name, text); } } @@ -1222,4 +1233,64 @@ fn reg64_liveness_rule_refuses_the_subset_fold_and_passes_the_full_chain() { let mut chain = epoch_reg64_of_devnet(); chain.program.class = chain.program.class.with_reg64_chain(); assert_eq!(check_window_liveness(&chain.program), Ok(()), "the full chain keeps all 64 registers live across the chain"); + // wired into the acceptance (8 October 2026): `accept::check` refuses the arithmetic-only window as a dead register + // and never refuses the chain program for liveness; the plain pack's verdict does not move + use igneum_pow::accept::check; + assert!(matches!(check(&window.program), Err(Reject::DeadWindowRegister { .. })), "the acceptance refuses the arithmetic-only window"); + assert!(!matches!(check(&chain.program), Err(Reject::DeadWindowRegister { .. }) | Err(Reject::NotAWindow)), "the chain program is never refused for liveness"); + assert!(check(&plain.program).is_ok(), "the pinned devnet program's verdict does not move"); +} + +/// The external review's A07 (8 October 2026): the bound hash of a nonce is the lane of the aligned 32-nonce group that +/// contains it, in the verifier by construction (`Epoch::hash_bound`), across the group's tail and the low-32 rollover; +/// the high 32 bits of the nonce enter the init words, so two nonces 2^32 apart never share a hash. The launchers +/// refuse a job whose nonce_start is not 32-aligned or whose count is not a multiple of 32 (worker.cpp's job line). +#[test] +fn a07_bound_hash_is_group_aligned_across_tails_and_rollover() { + use igneum_pow::bind::block_init_words; + let e = epoch("mx8-devnet-epoch0"); + let prehash = [0x5au8; 32]; + for &n in &[0u64, 1, 2, 31, 32, 33, 63, (1u64 << 32) - 1, 1u64 << 32, (1u64 << 32) + 1, u64::MAX - 1, u64::MAX] { + let base = n & !31; + let warp = e.hash_warp_bound(&prehash, base); + assert_eq!(e.hash_bound(&prehash, n), warp[(n & 31) as usize], "nonce {n}: the lane of its aligned group"); + assert_eq!(e.hash_warp_bound(&prehash, n), warp, "nonce {n}: the group is the same from any of its nonces"); + let distinct: std::collections::HashSet = warp.iter().copied().collect(); + assert!(distinct.len() >= 31, "nonce {n}: the lanes of a group are distinct hashes"); + } + assert_ne!(e.hash_bound(&prehash, 2), e.hash_bound(&prehash, 2 + (1u64 << 32)), "the high 32 bits reach the hash"); + assert_ne!(block_init_words(&prehash, 2), block_init_words(&prehash, 2 + (1u64 << 32))); +} + +/// The external review's A04 (8 October 2026): the acceptance executes the shadow block as the hash does (class v4 +/// sub-version 3), so a shadow that zeroes the registers fails acceptance; the pinned program with its own shadow passes. +#[test] +fn a04_a_zeroing_shadow_fails_acceptance() { + use igneum_pow::accept::check; + // a class v4 program (the shadow block is class v4's): the same seed and era as the lib's program_classes test + let era = [7u8; 32]; + let p = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era)); + assert!(check(&p).is_ok(), "the class v4 program of the genesis seed passes"); + let mut z = p.clone(); + assert!(!z.shadow.is_empty(), "class v4 has a shadow block"); + for ins in z.shadow.iter_mut() { + ins.op = Op::Xor; + ins.src = ins.dst; + } + assert!(check(&z).is_err(), "a shadow of xor d, d zeroes every register it touches and must fail"); +} + +/// The external review's A06 (8 October 2026): the pack's identity.json authenticates every kernel text by BLAKE2b-256. +#[test] +fn a06_program_json_hashes_every_kernel_text() { + let e = epoch("mx8-devnet-epoch0"); + let out = export_pack(&e, &day_label("mx8-devnet-epoch0"), "test"); + let file = |n: &str| out.files.iter().find(|(f, _)| f == n).map(|(_, t)| t.clone()).unwrap(); + let j: Value = serde_json::from_str(&file("identity.json")).unwrap(); + let h = j["kernel_blake2b256"].as_object().expect("kernel_blake2b256"); + assert_eq!(j["program_id"].as_str().unwrap(), format!("{:#018x}", e.program.program_id())); + for n in ["kernel.cu", "kernel.cl", "program.metal", "program_bound.metal", "kernel_bound.cu", "kernel_bound.cl"] { + let want: String = igneum_pow::blake2b::blake2b_256(&[file(n).as_bytes()]).iter().map(|x| format!("{x:02x}")).collect(); + assert_eq!(h[n].as_str().unwrap(), want, "{n}"); + } } diff --git a/igneum-pow/tests/v6fold.rs b/igneum-pow/tests/v6fold.rs index 956ae5b86..c66c73d65 100644 --- a/igneum-pow/tests/v6fold.rs +++ b/igneum-pow/tests/v6fold.rs @@ -8,7 +8,7 @@ use igneum_pow::accept::{index_bit_sigma, index_bit_sigma_max, ACCEPT_UNITS_DIST use igneum_pow::emit::export_pack; use igneum_pow::generator::{ candidate_class, generate_era_generator, LoadClass, Op, Program, ProgramClass, EraParams, INSTR_COUNT, NONLOAD_WEIGHTS, - NONLOAD_WEIGHTS_RW, NONLOAD_WEIGHTS_RW2, NONLOAD_WEIGHTS_RW2_SUM, NONLOAD_WEIGHTS_RW_SUM, V3_ALLOWED, V4_CLASS, V5_CLASS, + NONLOAD_WEIGHTS_RW, NONLOAD_WEIGHTS_RW2, NONLOAD_WEIGHTS_RW2_SUM, NONLOAD_WEIGHTS_RW_SUM, V3_ALLOWED, V3_CLASS, V4_CLASS, V5_CLASS, }; use igneum_pow::seed::seed_words_from_bytes; use igneum_pow::verify::{load_index, stride, DatasetGeom, Epoch, INDEX_FOLD_SHIFT}; @@ -182,6 +182,123 @@ fn class_v6_all_flags_parse_name_and_id() { assert!(p.class.era.unwrap().fold); } +/// 1c. D2 "layer 8 off" (8 October 2026): "+nowin" parses in any order and names back inside "+fold"/"+rw"; the draw +/// keeps every instruction of the class without the flag and sets every load site's window to the whole dataset; +/// the id moves; the era and the stride do not. +#[test] +fn nowin_removes_the_window_layer_and_nothing_else() { + let c = LoadClass::parse("mx8+sh256x27+state+nowin+fold+rw").unwrap(); + assert!(c.nowin && c.fold && c.rw == 1 && c.state); + assert_eq!(c.name(), "mx8+sh256x27+state+nowin+fold+rw"); + assert_eq!(LoadClass::parse("mx8+sh256x27+state+fold+rw+nowin"), Some(c)); + assert_eq!(LoadClass::parse("mx8+sh256x27+state+reg64c+nowin").unwrap().name(), "mx8+sh256x27+state+reg64c+nowin"); + assert!(!V5_CLASS.nowin); + let era = f8_bytes("era", 4); + let seed = f8_bytes("program", 4); + // both sides of the pair draw under the v6 rules (the five shuffle slots and the mad operand rule enter the + // stream for every class with a v6 flag), so the pair is the fold class with and without layer 8 off + let plain = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_fold(), &era, &V3_ALLOWED)); + let off = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_fold().with_nowin(), &era, &V3_ALLOWED)); + assert_ne!(plain.program_id(), off.program_id(), "the id carries the flag"); + assert_eq!(plain.instrs.len(), off.instrs.len()); + let mut windows_plain = 0; + for (a, b) in plain.instrs.iter().zip(off.instrs.iter()) { + assert_eq!((a.op, a.dst, a.src, a.src2, a.imm, a.imm2, a.rot, a.bit, a.mask, a.width), (b.op, b.dst, b.src, b.src2, b.imm, b.imm2, b.rot, b.bit, b.mask, b.width), "the instruction is the class's without the flag"); + assert_eq!((b.win, b.off), (0, 0), "every site reads the whole dataset"); + if a.win != 0 { + windows_plain += 1; + } + } + assert!(windows_plain > 0, "the plain draw has at least one shrunk window on this seed"); + assert_eq!(plain.shadow, off.shadow); + assert_eq!(plain.class.era.map(|e| EraParams { fold: e.fold, ..e }), off.class.era.map(|e| EraParams { fold: e.fold, ..e }), "the era draw is untouched"); +} + +/// The external review's A02 and A08 (8 October 2026), fixed by construction for every class carrying a v6 flag: +/// every candidate of 64 seeds (attempt 0, no acceptance pass) carries shuffles over all five lane dimensions, and no +/// `mad` names its destination as its second source; the plain class v5 draw is untouched (the pinned pack test +/// reads that); the v6 draw's id carries the rule. +#[test] +fn v6_draw_connects_all_lanes_and_keeps_mad_bijective_by_construction() { + let era = f8_bytes("era", 4); + let classes = [V5_CLASS.with_fold(), V5_CLASS.with_rw(1), V5_CLASS.with_nowin(), LoadClass::parse("mx8+sh256x27+state+reg64c+fold+rw").unwrap()]; + for c in classes { + assert!(c.is_v6()); + for k in 0..64u32 { + let seed = f8_bytes("program", k); + let p = candidate_class("p-a02", &seed, 0, LoadClass::era(c, &era, &V3_ALLOWED)); + let mut dims = [false; 5]; + for ins in &p.instrs { + if ins.op == Op::Shfl { + dims[ins.mask.trailing_zeros() as usize] = true; + } + if ins.op == Op::Mad { + assert_ne!(ins.src2, ins.dst, "seed {k} class {}: a mad with src2 == dst", c.name()); + } + } + assert!(dims.iter().all(|&d| d), "seed {k} class {}: shuffle dimensions {:?}", c.name(), dims); + assert_eq!(p.instrs.len(), 64); + assert_eq!(p.instrs.iter().filter(|i| i.op.is_load()).count(), 16, "the load slots are the class's"); + } + } + assert!(!V5_CLASS.is_v6() && !V3_CLASS.is_v6()); + // a plain class v5 candidate of the same seed can lack a dimension: the rule is the v6 construction's, not a filter + let seed = f8_bytes("program", 4); + let a = candidate_class("p-a02", &seed, 0, LoadClass::era(V5_CLASS, &era, &V3_ALLOWED)); + let b = candidate_class("p-a02", &seed, 0, LoadClass::era(V5_CLASS.with_fold(), &era, &V3_ALLOWED)); + assert_ne!(a.program_id(), b.program_id()); +} + +/// Lane D's finding (8 October 2026): every class v6 flag is set aside by the acceptance's class v4 shape predicate, +/// so a +nowin, +reg64 or +reg64c program is judged by the full rule (the fresh-source rule, the saturated-source +/// check, the index floors and the attempt cap with the last resort), not by the class v2 parts; the draw's source +/// rule reads the same shape. +#[test] +fn every_v6_flag_keeps_the_class_v4_acceptance_shape() { + use igneum_pow::accept::is_class_v4_shape; + use igneum_pow::generator::max_attempts_for; + let all = LoadClass::parse("mx8+sh256x27+state+reg64c+nowin+fold+rw").unwrap(); + for c in [V5_CLASS, V5_CLASS.with_nowin(), V5_CLASS.with_reg64(), V5_CLASS.with_reg64().with_reg64_chain(), V5_CLASS.with_fold(), all] { + assert!(is_class_v4_shape(&c), "{}", c.name()); + assert!(is_class_v4_shape(&LoadClass::era(c, &f8_bytes("era", 4), &V3_ALLOWED)), "{} under an era", c.name()); + assert_eq!(max_attempts_for(&c), max_attempts_for(&V5_CLASS), "{}: the same attempt cap and last resort", c.name()); + } + assert!(!is_class_v4_shape(&V3_CLASS) && !is_class_v4_shape(&LoadClass::V2)); + // lane D's second finding: an era drawn from a width set with more than one width writes the drawn width into + // `mix`; the shape (and so the full rule) must hold at every width of the family + let wide = LoadClass::era(V5_CLASS, &f8_bytes("era", 4), &[1u8, 4, 16]); + assert!(wide.era.is_some()); + assert!(is_class_v4_shape(&wide), "{} (mix {:?})", wide.name(), wide.mix); + assert!(is_class_v4_shape(&LoadClass::era(all, &f8_bytes("era", 9), &[1u8, 4, 16]))); +} + +/// Review B's F03 (8 October 2026): class v6 is a program class of its own, generator 6: the object class names it, +/// every class carrying a v6 flag draws as generator 6 under an era, the pack's program_class reads "v6", and the +/// class v5 draw and its generator number do not move. +#[test] +fn program_class_v6_is_generator_6() { + use igneum_pow::generator::{era_generator_of, GENERATOR_VERSION_V5, GENERATOR_VERSION_V6, V6_CLASS}; + assert_eq!(ProgramClass::parse("v6"), Some(ProgramClass::V6)); + assert_eq!(ProgramClass::V6.name(), "v6"); + assert_eq!(ProgramClass::V6.generator_version(), GENERATOR_VERSION_V6); + assert_eq!(ProgramClass::from_generator(6), Some(ProgramClass::V6)); + assert!(ProgramClass::V6.has_state()); + assert_eq!(ProgramClass::V6.load_class(), V6_CLASS); + assert_eq!(V6_CLASS.name(), "mx8+sh256x27+state+reg64c+fold+rw"); + assert_eq!(ProgramClass::of_load_class(&V6_CLASS), Some(ProgramClass::V6)); + assert_eq!(ProgramClass::of_load_class(&V6_CLASS.with_nowin()), Some(ProgramClass::V6)); + assert_eq!(ProgramClass::of_load_class(&V5_CLASS), Some(ProgramClass::V5)); + let era = f8_bytes("era", 4); + for c in [V6_CLASS, V6_CLASS.with_nowin(), V5_CLASS.with_fold(), V5_CLASS.with_rw(1), V5_CLASS.with_nowin()] { + assert_eq!(era_generator_of(&LoadClass::era(c, &era, &V3_ALLOWED)), GENERATOR_VERSION_V6, "{}", c.name()); + } + assert_eq!(era_generator_of(&LoadClass::era(V5_CLASS, &era, &V3_ALLOWED)), GENERATOR_VERSION_V5); + let seed = f8_bytes("program", 4); + let p = candidate_class("p-f03", &seed, 0, LoadClass::era(V6_CLASS, &era, &V3_ALLOWED)); + assert!(p.class.name().starts_with("mx8-era") && p.class.name().ends_with("+sh256x27+state+reg64c+fold+rw"), "{}", p.class.name()); + // candidate_class draws without stamping the generator; the era path stamps 6 (era_generator_of above) +} + /// 2. The fold's form: `y = x * M; y ^= y >> 16; y = rotl(y, R)`, and nothing else moves. The plain era's stride is /// `rotl(x * M, R)` byte for byte. #[test] @@ -201,14 +318,18 @@ fn fold_form_and_the_plain_stride_unchanged() { assert!(differ > 9_000, "the fold moved {differ} of 10,000 addresses"); // the program of a fold class is the program of the plain class: the same instructions when the same attempt // is accepted (the fold does not enter the draw; the indices it moves can move (c'') and (c''') verdicts) - let p = f8_program(4, ProgramClass::V4, false); - let f = f8_program(4, ProgramClass::V4, true); - if p.attempt == f.attempt { - assert_eq!(p.instrs, f.instrs); - assert_eq!(p.shadow, f.shadow); - } - assert_eq!(f.class.name(), format!("{}+fold", p.class.name())); + // since the review's draw rules (8 October 2026) a fold class draws under the v6 rules, so the pair is the v6 + // re-weight class with and without the fold: the same instructions, the fold moving the addresses alone + let era = f8_bytes("era", 4); + let seed = f8_bytes("program", 4); + let p = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_rw(1), &era, &V3_ALLOWED)); + let f = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_rw(1).with_fold(), &era, &V3_ALLOWED)); + assert_eq!(p.instrs, f.instrs); + assert_eq!(p.shadow, f.shadow); assert_ne!(p.program_id(), f.program_id()); + // the name orders the fold inside the re-weight suffix ("...+state+fold+rw"); both spellings parse to the class + assert!(f.class.name().ends_with("+sh256x27+state+fold+rw"), "{}", f.class.name()); + assert_eq!(LoadClass::parse("mx8+sh256x27+state+rw+fold"), Some(V5_CLASS.with_rw(1).with_fold())); // and load_index reads the fold through the era let ins = p.instrs.iter().find(|i| i.op == Op::Load).unwrap(); let (pe, fe) = (p.class.era.unwrap(), f.class.era.unwrap()); @@ -285,8 +406,11 @@ fn plain_class_draw_is_byte_identical_to_the_pinned_pack() { let e = Epoch { program, dataset }; assert_eq!(e.program.program_id(), PINNED_ID); let out = export_pack(&e, &format!("bytes:{DAY_HEX}"), SOURCE); - assert_eq!(out.files.len(), 12, "the pack's twelve files"); + assert_eq!(out.files.len(), 13, "the pack's thirteen files"); for (name, text) in &out.files { + if name == "identity.json" { + continue; // A06's identity file is new beside the pinned twelve + } let want = pinned(name); assert!(text == &want, "mx8-devnet-epoch0/{name} differs from the default path's export"); } diff --git a/proto-cuda/nvrtc/packfile-fuzz.c b/proto-cuda/nvrtc/packfile-fuzz.c new file mode 100644 index 000000000..9cafb5519 --- /dev/null +++ b/proto-cuda/nvrtc/packfile-fuzz.c @@ -0,0 +1,57 @@ +/* packfile-fuzz.c: POW-01's malformed-input half for the pack reader (8 October 2026). Copies a pack directory, mutates + * one of its files N times (a flipped byte, a truncation, an inserted byte, a doubled slice, random bytes) and calls + * pf_load on the copy each time; the run passes when every call returns 0 (refused with a message) or 1 (accepted), + * and the process never crashes. One line per 1,000 rounds, a RESULT line at the end. + * cc -O2 -o packfile-fuzz packfile-fuzz.c && ./packfile-fuzz + */ +#include "packfile.h" +#include + +static uint64_t rs = 0x1234567; +static uint64_t rnext(void) { rs += 0x9e3779b97f4a7c15ull; uint64_t z = rs; z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ull; z = (z ^ (z >> 27)) * 0x94d049bb133111ebull; return z ^ (z >> 31); } +static size_t below(size_t n) { return n ? (size_t)(rnext() % n) : 0; } + +static int copy_file(const char* from, const char* to) { + size_t n = 0; char* b = pf_read_file(from, &n); if (!b) { remove(to); return 0; } + FILE* f = fopen(to, "wb"); if (!f) { free(b); return 0; } + fwrite(b, 1, n, f); fclose(f); free(b); return 1; +} + +static void write_mutated(const char* src, const char* dst) { + size_t n = 0; char* b = pf_read_file(src, &n); + if (!b) { remove(dst); return; } /* a pack without this file: the mutation is its absence */ + size_t cap = n + 400; char* v = (char*)malloc(cap ? cap : 1); size_t len = n; if (n) memcpy(v, b, n); + switch (below(6)) { + case 0: if (len) v[below(len)] ^= (char)(1 << below(8)); break; + case 1: len = below(len + 1); break; + case 2: { size_t i = below(len + 1); memmove(v + i + 1, v + i, len - i); v[i] = (char)rnext(); len++; } break; + case 3: if (len > 1) { size_t a = below(len), bl = below(len - a); if (bl > 300) bl = 300; memmove(v + a + bl, v + a, len - a); memcpy(v + a, b + a, bl); len += bl; } break; + case 4: len = below(300); for (size_t i = 0; i < len; i++) v[i] = (char)rnext(); break; + default: if (len) v[below(len)] = (char)rnext(); break; + } + FILE* f = fopen(dst, "wb"); if (f) { fwrite(v, 1, len, f); fclose(f); } + free(v); free(b); +} + +int main(int argc, char** argv) { + if (argc < 4) { fprintf(stderr, "usage: packfile-fuzz \n"); return 2; } + const char* pack = argv[1]; long rounds = atol(argv[2]); const char* scratch = argv[3]; + const char* files[] = { "program.h", "vectors.h", "seeds.txt", "memhard.h" }; + mkdir(scratch, 0755); + char from[1024], to[1024]; + for (int i = 0; i < 4; i++) { snprintf(from, sizeof from, "%s/%s", pack, files[i]); snprintf(to, sizeof to, "%s/%s", scratch, files[i]); copy_file(from, to); } + PfPack pk; char err[512]; + int base = pf_load(pack, &pk, err, sizeof err); + printf("base pack %s: %s%s\n", pack, base ? "accepted" : "refused: ", base ? "" : err); + long accepted = 0, refused = 0; + for (long r = 0; r < rounds; r++) { + int which = (int)below(4); + for (int i = 0; i < 4; i++) { snprintf(from, sizeof from, "%s/%s", pack, files[i]); snprintf(to, sizeof to, "%s/%s", scratch, files[i]); if (i == which) write_mutated(from, to); else copy_file(from, to); } + memset(&pk, 0, sizeof pk); err[0] = 0; + int ok = pf_load(scratch, &pk, err, sizeof err); + if (ok) accepted++; else refused++; + if ((r + 1) % 1000 == 0) printf("round %ld: accepted %ld refused %ld\n", r + 1, accepted, refused); + } + printf("RESULT packfile-fuzz pack=%s rounds=%ld accepted=%ld refused=%ld crashes=0 verdict=PASS\n", pack, rounds, accepted, refused); + return 0; +} diff --git a/proto-cuda/nvrtc/packfile.h b/proto-cuda/nvrtc/packfile.h index f99ddd87d..edf156948 100644 --- a/proto-cuda/nvrtc/packfile.h +++ b/proto-cuda/nvrtc/packfile.h @@ -333,11 +333,13 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) { * kernel text, so this loader needs nothing new beyond the number and the class token), generator 5 is class v5 * (proof of stored state, 7 October 2026: class v4 over a dataset keyed by the window's state leaves, leaves.bin, * which the host uploads for igneum_build; the kernel text carries the leaf read). */ - if (pk->generator != 2 && pk->generator != 3 && pk->generator != 4 && pk->generator != 5) { - char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2, 3, 4 or 5)", (unsigned)pk->generator); + if (pk->generator != 2 && pk->generator != 3 && pk->generator != 4 && pk->generator != 5 && pk->generator != 6) { + char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2, 3, 4, 5 or 6)", (unsigned)pk->generator); free(prog); return pf_fail(err, cap, m); } - strcpy(pk->programClass, pk->generator == 5 ? "v5" : pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2"); + /* generator 6 (class v6, the Igneum 2.0 D1 object, 8 October 2026): class v5's stored-state dataset with the v6 rules in the kernel text; + * the worker runs it as it runs class v5 (the shadow block, the state leaves), the pack's texts carry the rest */ + strcpy(pk->programClass, pk->generator == 6 ? "v6" : pk->generator == 5 ? "v5" : pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2"); { /* Counter ASIC 3.0 (6 October 2026): the shadow block marks class v4. A generator 3 pack with IGNEUM_SHADOW_INSTRS * is a v4 program stamped as v3 (the old export path; it carried the v3 control's program id) and is refused; @@ -345,7 +347,7 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) { uint32_t shadow = 0; if (!pf_define_u32(prog, "IGNEUM_SHADOW_INSTRS", &shadow)) shadow = 0; if (pk->generator == 4 && shadow == 0) { free(prog); return pf_fail(err, cap, "program pack generator 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack"); } - if (pk->generator == 5 && shadow == 0) { free(prog); return pf_fail(err, cap, "program pack generator 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack (class v5 is class v4 over the state leaves)"); } + if ((pk->generator == 5 || pk->generator == 6) && shadow == 0) { free(prog); return pf_fail(err, cap, "program pack generator 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack (class v5 is class v4 over the state leaves)"); } if (pk->generator == 3 && shadow != 0) { /* a generator 2 pack with a shadow is the measurement ladder (a class-bearing id) and loads */ char m[220]; snprintf(m, sizeof(m), "program pack generator %u with a shadow block (IGNEUM_SHADOW_INSTRS %u): a class v4 program is generator 4 (export the pack as class v4)", (unsigned)pk->generator, (unsigned)shadow); free(prog); return pf_fail(err, cap, m); @@ -365,8 +367,8 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) { pk->stateLeaves = 0; pk->stateLeavesFnv = 0; pk->stateRootHex[0] = 0; pk->stateBlockHex[0] = 0; strcpy(pk->stateLeavesFile, "leaves.bin"); if (!pf_define_u32(prog, "IGNEUM_STATE_LEAVES", &pk->stateLeaves)) pk->stateLeaves = 0; - if (pk->generator == 5 && pk->stateLeaves == 0) { free(prog); return pf_fail(err, cap, "program pack generator 5 (class v5) without IGNEUM_STATE_LEAVES: no state leaves to key the dataset (export the pack with --state)"); } - if (pk->generator != 5 && pk->stateLeaves != 0) { + if ((pk->generator == 5 || pk->generator == 6) && pk->stateLeaves == 0) { free(prog); return pf_fail(err, cap, "program pack generator 5 (class v5) without IGNEUM_STATE_LEAVES: no state leaves to key the dataset (export the pack with --state)"); } + if (pk->generator != 5 && pk->generator != 6 && pk->stateLeaves != 0) { char m[200]; snprintf(m, sizeof(m), "program pack generator %u carries IGNEUM_STATE_LEAVES %u: state leaves belong to class v5 (generator 5)", (unsigned)pk->generator, (unsigned)pk->stateLeaves); free(prog); return pf_fail(err, cap, m); } diff --git a/proto-metal/main.swift b/proto-metal/main.swift index 6c7e48823..87cda6f40 100644 --- a/proto-metal/main.swift +++ b/proto-metal/main.swift @@ -2992,9 +2992,10 @@ final class ServeDataset { /// Counter ASIC 2.0 and 3.0: the classes this worker runs from a prepared pack only (never from the Swift version 2 /// generator): class v3 (generator 3) and class v4 (generator 4, class v3 plus the latency-shadow block, which is in /// the pack's own program_bound.metal). Each carries an era seed. -func isPackClass(_ cls: String) -> Bool { cls == "v3" || cls == "v4" || cls == "v5" } // class v5 (7 October 2026): class v4 over the state leaves, from a pack +func isPackClass(_ cls: String) -> Bool { cls == "v3" || cls == "v4" || cls == "v5" || cls == "v6" } // class v5 (7 October 2026): class v4 over the state leaves, from a pack; class v6 (8 October 2026): class v5's stored-state dataset with the v6 rules in the kernel text (packfile.h db63e1e36: run as class v5) /// The class name of a pack's IGNEUM_GENERATOR (packfile.h's rule). -func packClassOf(generator: UInt32) -> String { generator == 5 ? "v5" : generator == 4 ? "v4" : generator == 3 ? "v3" : "v2" } +func packClassOf(generator: UInt32) -> String { generator == 6 ? "v6" : generator == 5 ? "v5" : generator == 4 ? "v4" : generator == 3 ? "v3" : "v2" } +func isStateClass(_ cls: String) -> Bool { cls == "v5" || cls == "v6" } // the classes keyed by the state leaves // The resident programs and datasets, shared by the job loop (main thread) and the prepare queue (background). final class ServeStore { @@ -3085,7 +3086,7 @@ func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [U } func refuse(_ why: String) -> NSError { NSError(domain: "pack", code: 2, userInfo: [NSLocalizedDescriptionKey: "pack \(dir): \(why)"]) } let generator = defineU32("IGNEUM_GENERATOR") ?? 1 - guard generator == 2 || generator == 3 || generator == 4 || generator == 5 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2, 3, 4 or 5)") } + guard generator == 2 || generator == 3 || generator == 4 || generator == 5 || generator == 6 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2, 3, 4, 5 or 6)") } let packClass = packClassOf(generator: generator) if let named = defineStr("IGNEUM_PROGRAM_CLASS"), named != packClass { throw refuse("program pack IGNEUM_PROGRAM_CLASS \"\(named)\" does not match IGNEUM_GENERATOR \(generator)") } // Counter ASIC 3.0 (6 October 2026): the shadow block marks class v4 (packfile.h's rule): a generator 3 pack with @@ -3093,9 +3094,9 @@ func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [U let shadow = defineU32("IGNEUM_SHADOW_INSTRS") ?? 0 if generator == 4 && shadow == 0 { throw refuse("program pack generator 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack") } // class v5 (7 October 2026) is class v4 over the state leaves: the shadow block and IGNEUM_STATE_LEAVES both mark it - if generator == 5 && shadow == 0 { throw refuse("program pack generator 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack") } - if generator == 5 && (defineU32("IGNEUM_STATE_LEAVES") ?? 0) == 0 { throw refuse("program pack generator 5 (class v5) without IGNEUM_STATE_LEAVES: no state leaves to key the dataset (export the pack with --state)") } - if generator != 5 && (defineU32("IGNEUM_STATE_LEAVES") ?? 0) != 0 { throw refuse("program pack generator \(generator) carries IGNEUM_STATE_LEAVES: state leaves belong to class v5 (generator 5)") } + if (generator == 5 || generator == 6) && shadow == 0 { throw refuse("program pack generator 5 (class v5) without IGNEUM_SHADOW_INSTRS: not a class v5 pack") } + if (generator == 5 || generator == 6) && (defineU32("IGNEUM_STATE_LEAVES") ?? 0) == 0 { throw refuse("program pack generator 5 (class v5) without IGNEUM_STATE_LEAVES: no state leaves to key the dataset (export the pack with --state)") } + if generator != 5 && generator != 6 && (defineU32("IGNEUM_STATE_LEAVES") ?? 0) != 0 { throw refuse("program pack generator \(generator) carries IGNEUM_STATE_LEAVES: state leaves belong to class v5 (generator 5)") } if generator == 3 && shadow != 0 { throw refuse("program pack generator \(generator) with a shadow block (IGNEUM_SHADOW_INSTRS \(shadow)): a class v4 program is generator 4 (export the pack as class v4)") } let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? "" if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass) (export the pack again)") } @@ -3162,7 +3163,9 @@ func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: Stri guard let fillFn = lib.makeFunction(name: "igneum_cache_fill"), let buildFn = lib.makeFunction(name: "igneum_build") else { throw refuse("memhard.metal lacks igneum_cache_fill or igneum_build") } let fillPipe = try gpu.device.makeComputePipelineState(function: fillFn) let buildPipe = try gpu.device.makeComputePipelineState(function: buildFn) - let words = 1 << datasetLog2 + // ds55 (the class v6 research packs, 8 October 2026): a non-power-of-two dataset carries IGNEUM_DATASET_WORDS (the + // buffer's size) and IGNEUM_DATASET_ITEMS beside IGNEUM_DATASET_LOG2 (the floor); the kernel text maps (src * words) >> 32 + let words = defineU64("IGNEUM_DATASET_WORDS").map { Int($0) } ?? (1 << datasetLog2) // Class v5 (docs/design/class-v5-stored-state.md, 7 October 2026): the window's state leaves, leaves.bin beside program.h // (IGNEUM_STATE_LEAVES leaves of 16 little-endian words), checked against the pack's count and FNV-1a 64 // (IGNEUM_STATE_LEAVES_FNV64) and bound as buffer 2 of igneum_build with the count in buffer 3 (packbench.swift's shape, @@ -3173,7 +3176,7 @@ func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: Stri return UInt64(programH[Range(m.range(at: 1), in: programH)!], radix: 16) } let stateLeaves = defineU32("IGNEUM_STATE_LEAVES") ?? 0 - if (packClass == "v5") != (stateLeaves > 0) { throw refuse(packClass == "v5" ? "class v5 pack without IGNEUM_STATE_LEAVES" : "a class \(packClass) pack carries IGNEUM_STATE_LEAVES \(stateLeaves): state leaves belong to class v5") } + if isStateClass(packClass) != (stateLeaves > 0) { throw refuse(isStateClass(packClass) ? "class v5 pack without IGNEUM_STATE_LEAVES" : "a class \(packClass) pack carries IGNEUM_STATE_LEAVES \(stateLeaves): state leaves belong to class v5") } var leavesBuf: MTLBuffer? = nil var nLeaves: UInt32 = 0 if stateLeaves > 0 { diff --git a/proto-metal/packbench.swift b/proto-metal/packbench.swift index a382ddd7d..6e8a8fa88 100644 --- a/proto-metal/packbench.swift +++ b/proto-metal/packbench.swift @@ -93,8 +93,12 @@ let cacheFnvWant = hex64(vj["cache_fnv1a64"] as! String) let hotFnvWant: UInt64? = (vj["hot_fnv1a64"] as? String).map(hex64) guard let device = MTLCreateSystemDefaultDevice(), let queue = device.makeCommandQueue() else { fail("no Metal device") } -let words = 1 << datasetLog2 -let mask = UInt32(words - 1) +// ds55 (the class v6 research packs, 8 October 2026): a non-power-of-two dataset carries IGNEUM_DATASET_WORDS and +// IGNEUM_DATASET_ITEMS beside IGNEUM_DATASET_LOG2 (the floor); the buffer is sized by the words, the items by ITEMS, +// and the kernel text maps an address as (src * words) >> 32, so the mask is the kernel's business, not this host's +let datasetWordsDefine = defineU64("IGNEUM_DATASET_WORDS") +let words = datasetWordsDefine.map { Int($0) } ?? (1 << datasetLog2) +let mask = UInt32(truncatingIfNeeded: (1 << datasetLog2) - 1) let cacheWords = 1 << cacheLog2 func compile(_ file: String) -> MTLLibrary { @@ -138,7 +142,7 @@ func fnv1a64(_ p: UnsafeRawPointer, _ n: Int) -> UInt64 { for i in 0..&1 | ForEach-Object { "$_" }) +$list | ForEach-Object { "RESULT list $_" } +$dev = $null +foreach ($l in $list) { if ($l -match '^\s*\[(\d+)\].*(Intel|Arc|B580)' -and $l -notmatch 'dup') { $dev = [int]$Matches[1]; break } } +if ($null -eq $dev) { "RESULT error no Intel Arc device in --list (the B580 is off the bus or has no OpenCL runtime: the Intel v5 row stays OWED)"; Summary 'failed' @{ error = 'no intel arc' }; exit 2 } +"RESULT device $dev Intel Arc (list from $listExe)" +function Workers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" }) } +$w = @(Workers) +$loaded = ($w | Where-Object { $_ -match "igneum-worker-opencl.*--device\s+$dev(\s|$)" }).Count -gt 0 +"RESULT workers_before $(Stamp) $($w -join ' ')" +$state = if ($loaded) { 'loaded' } else { 'quiet' } +"RESULT context card_state=$state (a fingerprint gate: the load changes the rate row, never the bytes)" +$rows = @{} +$fpOk = $false +foreach ($pk in @('hl-v6-foldrw', 'hl-v6-all')) { + $d = Join-Path $packs $pk + $t0 = Get-Date + "RESULT bench $pk start $(Stamp) cmd=igneum-worker-opencl.exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches 5" + $out = @(& $exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches 5 2>&1 | ForEach-Object { "$_" }) + $code = $LASTEXITCODE + $secs = [int]((Get-Date) - $t0).TotalSeconds + # the whole stdout, line by line (the 09:36 UK run kept one line per pack and lost the host's exchange, kernel, + # rotate-fold and build-log lines, which the diagnosis needed) + foreach ($l in $out) { "RESULT bench $pk out $l" } + $res = $out | Where-Object { $_ -match '^RESULT ' } | Select-Object -Last 1 + $fp = ''; $mhs = ''; $check = '' + if ($res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] } + if ($res -match 'mhs=([0-9.]+)') { $mhs = $Matches[1] } + if ($res -match 'check=(\w+)') { $check = $Matches[1] } + $rows[$pk] = [ordered]@{ exit = $code; seconds = $secs; fingerprint = $fp; mhs = $mhs; check = $check; card_state = $state } + if ($pk -eq 'hl-v6-foldrw') { + $fpOk = ($fp -eq $expected -and $check -eq 'PASS') + "RESULT partner fingerprint=$fp expected=$expected match=$fpOk check=$check mhs=$mhs card_state=$state exit=$code seconds=$secs $(Stamp)" + } else { + # the all pack: with the window's OpenCL text (the second zip) a fingerprint, expected the CUDA reference e8f4f3289c6ee1fc + $allOk = ($fp -eq 'e8f4f3289c6ee1fc' -and $check -eq 'PASS') + "RESULT all-pack fingerprint=$fp expected=e8f4f3289c6ee1fc match=$allOk check=$check mhs=$mhs card_state=$state exit=$code seconds=$secs $(Stamp)" + } +} +"RESULT workers_after $(Stamp) $((@(Workers)) -join ' ')" +Summary $(if ($fpOk) { 'done' } else { 'failed' }) @{ expected = $expected; partner = $rows['hl-v6-foldrw']; all_pack = $rows['hl-v6-all']; device = $dev; kit = $kitId } +exit $(if ($fpOk) { 0 } else { 1 }) diff --git a/tools/class-v5/kits-remote.sh b/tools/class-v5/kits-remote.sh index 15c43d55c..e355bd5ab 100755 --- a/tools/class-v5/kits-remote.sh +++ b/tools/class-v5/kits-remote.sh @@ -13,9 +13,9 @@ # # Takes one of the box's build slots through infra/build-server/remote-run.sh (bs_remote_run), box 1 by default (the # build-server lane's word of 7 October 2026, 19:4x BST: the kit builds on build-1 explicitly). The work itself is -# tools/class-v5/kits-on-box.sh, a script file run by path on the box (the inline-rm rule of 21:33 BST); when the +# tools/class-v5/kits-on-box.sh (or KITS_BOX_SCRIPT=kits-v6-on-box.sh for the class v6 kit), a script file run by path on the box (the inline-rm rule of 21:33 BST); when the # build-server lane's lease tool is in place, the same file goes through `/srv/builds/_bin/lease pool -- bash -# tools/class-v5/kits-on-box.sh ...` with the label "v5 kit". +# tools/class-v5/${KITS_BOX_SCRIPT:-kits-on-box.sh} ...` with the label "v5 kit". set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BS_TOOL=class-v5-kits @@ -34,9 +34,9 @@ ZIP="/srv/artefacts/packs/packs-ca3-v5-$STAMP.zip" bs_toolchain_check bs_sync_sources bs_log "sources at $WT (commit $BS_SHA on $BS_BRANCH); building the class v5 kits on box $BOX" -# the body runs from a script FILE at the worktree's mirror checkout (tools/class-v5/kits-on-box.sh, carried by the commit), never +# the body runs from a script FILE at the worktree's mirror checkout (tools/class-v5/${KITS_BOX_SCRIPT:-kits-on-box.sh}, carried by the commit), never # an inline string: main's rule of 7 October 2026, 21:33 BST (no rm, find -delete or truncation inside a bash -c string) -CMD="cd '$WT' && bash tools/class-v5/kits-on-box.sh '$STAMP' '$SKIP_EMU'" +CMD="cd '$WT' && bash tools/class-v5/${KITS_BOX_SCRIPT:-kits-on-box.sh} '$STAMP' '$SKIP_EMU'" set +e BR_KIND=build BR_COMMAND="class v5 kits (packfile-test, emu --check v5, Linux and Windows workers, kit zip)" BR_TARGET="x86_64-linux+windows" BR_ARTEFACTS="" \ bs_remote_run "$WT" "$BS_WT class-v5 kits" "$CMD" 2>&1 | tee "/tmp/v5-kits-$STAMP.log" | grep -E '^(STEP|FAIL|KIT|BIN|LOGS|check PASS|self-test|packfile-test|emu:| v5|build-remote: RESULT)' diff --git a/tools/class-v5/kits-v6-on-box.sh b/tools/class-v5/kits-v6-on-box.sh new file mode 100755 index 000000000..845cdbd5c --- /dev/null +++ b/tools/class-v5/kits-v6-on-box.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# The class v5 kit build as it runs ON igneum-build-1 (called by tools/class-v5/kits-remote.sh through the build-server +# library's remote runner, from the worktree's mirror checkout, so this file travels with the commit). A script file by +# path, never an inline string: main's rule of 7 October 2026, 21:33 BST (no rm, find -delete or truncation inside a +# `bash -c` string; tools/ci/inline-rm-check.sh). What it does, in order: the pack loader test with the class v5 cases, +# the Linux and Windows builds of the two one-click workers, the CPU emulation's --check on the class v5 pack, then the +# kit zip at /srv/artefacts/packs/packs-ca3-v5-.zip with SHA256SUMS. Logs land in /srv/builds/_log/v5-class/kits-. +# bash tools/class-v5/kits-on-box.sh (cwd = the worktree root on the box) +set -u +STAMP="${1:?stamp}"; SKIP_EMU="${2:-0}" +ZIP="/srv/artefacts/packs/packs-class-v6-$STAMP.zip" +LOGDIR="/srv/builds/_log/v5-class/kits-$STAMP" +T=$(mktemp -d /tmp/v5-kits.XXXXXX); S=$T/stage; mkdir -p "$S/bin/linux" "$S/bin/windows" "$S/src" "$S/packs" "$S/tools" "$LOGDIR" +rc=0 +step() { echo "STEP $1 $(date -u +%H:%M:%SZ)"; } +fail() { echo "FAIL $1"; rc=1; } +finish() { cp "$T"/*.log "$LOGDIR"/ 2>/dev/null; echo "LOGS $LOGDIR"; rm -rf "$T"; } +# class v6 (8 October 2026, the coordinator's order): the all-together pack and its known-failed partner come from the hash +# lane's tgz files on build-1 (V6_ALL and V6_PARTNER, default hl-v6-all and hl-v6-foldrw), extracted beside the stage; +# the emulation check runs pack A = the all pack against pack B = the partner, and the two ids must differ +V6_ALL="${V6_ALL:-hl-v6-all}"; V6_PARTNER="${V6_PARTNER:-hl-v6-foldrw}" +PK=$T/v6packs; mkdir -p "$PK" +for p in "$V6_ALL" "$V6_PARTNER"; do tar -xzf "/srv/artefacts/packs/$p.tgz" -C "$PK" 2>/dev/null || { echo "FAIL no pack tgz /srv/artefacts/packs/$p.tgz"; exit 1; }; done +rm -rf "$PK"/._* "$PK"/*/._* 2>/dev/null +V5="$PK/$V6_ALL"; V6P="$PK/$V6_PARTNER" +echo "PACKS $V6_ALL id $(grep -oE 'IGNEUM_PROGRAM_ID 0x[0-9a-f]+' "$V5/program.h" | awk '{print $2}') | $V6_PARTNER id $(grep -oE 'IGNEUM_PROGRAM_ID 0x[0-9a-f]+' "$V6P/program.h" | awk '{print $2}')" +[ "$(grep -oE 'IGNEUM_PROGRAM_ID 0x[0-9a-f]+' "$V5/program.h")" != "$(grep -oE 'IGNEUM_PROGRAM_ID 0x[0-9a-f]+' "$V6P/program.h")" ] || { echo "FAIL the all pack and its partner carry one id"; exit 1; } +# the test's third argument is its class v5 known-good (the checked-in Devnet 3 epoch 0 pack, generator 5, 11 leaves); the kit's all +# pack is generator 6 since the post-review export (class v6, 93 leaves) and fails that check by construction (the third kit build, +# 20:38 UK), so the checked-in pack is named and the v6 packs are read by the emulation check below +step packfile-test +cc -std=c99 -Wall -Wextra -Wno-unused-function -O1 -o "$T/packfile-test" proto-cuda/nvrtc/emu/packfile-test.c && "$T/packfile-test" proto-cuda/packs/igneum-devnet-v4-epoch0 proto-cuda/packs-ca3-v5/v5-dn3-epoch0 > "$T/packfile-test.log" 2>&1 || fail "packfile-test ($(tail -1 "$T/packfile-test.log"))" +grep -E '^(FAIL| v5 pack)' "$T/packfile-test.log"; grep -c '^ok' "$T/packfile-test.log" | sed 's/^/packfile-test ok lines /' +step linux-opencl-worker +gcc -std=c99 -O2 -Wall -Wextra -Wno-stringop-truncation -Wno-format-truncation -DIGNEUM_CL_DYNAMIC -DCL_TARGET_OPENCL_VERSION=120 -I proto-cuda/packs/igneum-devnet-v4-epoch0 -DIGNEUM_KERNEL_PATH='"kernel_bound.cl"' -o "$S/bin/linux/igneum-worker-opencl" proto-opencl/host.c -ldl -lpthread 2> "$T/cl-linux.log" || { fail "linux opencl worker"; head -20 "$T/cl-linux.log"; } +step linux-cuda-worker +g++ -std=c++17 -O2 -Wall -Wextra -I proto-cuda/nvrtc -I /usr/local/cuda/include -o "$S/bin/linux/igneum-worker-cuda" proto-cuda/nvrtc/worker.cpp -ldl -lpthread 2> "$T/cuda-linux.log" || { fail "linux cuda worker"; head -20 "$T/cuda-linux.log"; } +step windows-cuda-worker +x86_64-w64-mingw32-g++ -std=c++17 -O2 -Wall -Wextra -static -I proto-cuda/nvrtc -I /usr/local/cuda/include -o "$S/bin/windows/igneum-worker-cuda.exe" proto-cuda/nvrtc/worker.cpp 2> "$T/cuda-win.log" && x86_64-w64-mingw32-strip "$S/bin/windows/igneum-worker-cuda.exe" || { fail "windows cuda worker"; head -20 "$T/cuda-win.log"; } +step windows-opencl-worker +# the Khronos CL headers alone (never -I /usr/include: that puts glibc's stdint.h ahead of mingw's) +mkdir -p "$T/inc" && cp -r /usr/include/CL "$T/inc/" +x86_64-w64-mingw32-gcc -std=c99 -O2 -Wall -Wextra -Wno-stringop-truncation -Wno-format-truncation -static -DIGNEUM_CL_DYNAMIC -DCL_TARGET_OPENCL_VERSION=120 -I "$T/inc" -I proto-cuda/packs/igneum-devnet-v4-epoch0 -DIGNEUM_KERNEL_PATH='"kernel_bound.cl"' -o "$S/bin/windows/igneum-worker-opencl.exe" proto-opencl/host.c 2> "$T/cl-win.log" && x86_64-w64-mingw32-strip "$S/bin/windows/igneum-worker-opencl.exe" || { fail "windows opencl worker"; head -20 "$T/cl-win.log"; } +if [ "$SKIP_EMU" = 0 ]; then + step emu-check-v5 + # the CPU emulation (proto-cuda/nvrtc/emu/test.sh's build) with pack A = the class v5 pack and pack B = the v4 control + E=$T/emu; mkdir -p "$E" + emu_kernel() { { echo '#include '; echo '#include '; echo "namespace $2 {"; sed -E 's/([A-Za-z_0-9]+)<<<([^,]+), ([^>]+)>>>\(/emu_launch(\1, \2, \3, /' "$1/$3.cu"; echo "}"; } > "$E/$3_$2.cpp"; g++ -std=c++17 -O2 -w -I proto-cuda/emu -I "$1" -c "$E/$3_$2.cpp" -o "$E/$3_$2.o"; } + emu_kernel "$V5" emu_pack_a kernel && emu_kernel "$V5" emu_pack_a kernel_bound && emu_kernel "$V6P" emu_pack_b kernel && emu_kernel "$V6P" emu_pack_b kernel_bound \ + && g++ -std=c++17 -O2 -Wall -Wextra -DIGNEUM_EMU -I proto-cuda/nvrtc -I /usr/local/cuda/include -c proto-cuda/nvrtc/worker.cpp -o "$E/worker.o" \ + && g++ -std=c++17 -O2 -Wall -Wextra -DIGNEUM_EMU -DIGNEUM_EMU_TWO_PACKS -I proto-cuda/nvrtc -I proto-cuda/emu -I /usr/local/cuda/include -c proto-cuda/nvrtc/emu/emu_backend.cpp -o "$E/emu_backend.o" \ + && g++ -std=c++17 -O2 -w -I proto-cuda/emu -c proto-cuda/emu/shim.cpp -o "$E/shim.o" \ + && g++ -o "$E/igneum-worker-cuda-emu" "$E"/*.o -pthread 2> "$T/emu-build.log" || { fail "emu build"; head -30 "$T/emu-build.log"; } + if [ -x "$E/igneum-worker-cuda-emu" ]; then + ( IGNEUM_EMU_PACK="$V5" IGNEUM_EMU_PACK2="$V6P" timeout 1500 nice -n 10 "$E/igneum-worker-cuda-emu" --check --pack "$V5" > "$T/emu-check.log" 2>&1 ) || fail "emu --check on the class v5 pack (rc $?)" + grep -E '^check PASS|self-test|FAIL|error' "$T/emu-check.log" | cut -c1-400 + fi +fi +if [ "$rc" != 0 ]; then echo "KIT not written: a step failed (rc $rc)"; finish; exit 1; fi +step stage +for p in "$V6_ALL" "$V6_PARTNER"; do mkdir -p "$S/packs/$p"; cp "$PK/$p"/* "$S/packs/$p/"; done +cp proto-cuda/nvrtc/worker.cpp proto-cuda/nvrtc/packfile.h proto-cuda/nvrtc/cuda_api.h proto-opencl/host.c proto-opencl/cl_dynamic.h proto-newpow/class-v5/bench.cu proto-newpow/class-v5/run.sh proto-metal/packbench.swift "$S/src/" +cp tools/class-v5/fleet-cuda-v5-bench.sh tools/class-v5/pc1-amd-v5-bench.ps1 "$S/tools/" +cat > "$S/README.txt" <<'R' +packs-class-v6: the class v6 kit (Igneum, 8 October 2026; docs/design/class-v5-stored-state.md section 0, the class v6 row): the all-together pack and its known-failed partner +packs/v5-dn3-epoch0 the first class v5 pack: Devnet 3 epoch 0, program id e5a4ac5978462156, 11 state leaves (leaves.bin, 704 B) under + state root 7e37a9fb19b154d32daf5bf30a50d339a75029fbc9eec9ea20e95439dba5a311, cache FNV-1a 64 7334fa46e5d972eb + expected fingerprint of the 2^24 outputs at base nonce 0: 82b19cbde8557ea5 (Metal, M5 Max, 18:07:09Z) on every platform +packs/v5-genesis the string-seed class v5 pack over the devnet's 93 leaves; packs/v4-genesis the class v4 control (sub-version 3) +bin/linux igneum-worker-cuda (NVRTC, libcuda + libnvrtc.so.12 at run time), igneum-worker-opencl (libOpenCL.so.1 at run time) +bin/windows igneum-worker-cuda.exe (nvcuda.dll + nvrtc64 at run time), igneum-worker-opencl.exe (OpenCL.dll at run time) +Every worker reads a class v5 pack's leaves.bin, checks it against the pack's IGNEUM_STATE_LEAVES_FNV64, uploads it for igneum_build and +frees it after the build; a pack without its leaves builds nothing. Self-test: cache head, last line and FNV; dataset head, last word and +64 samples; 96 vector lanes. Bench: igneum-worker-cuda --bench --pack packs/v5-dn3-epoch0 --batch-log2 24 (fleet-cuda-v5-bench.sh); +igneum-worker-opencl --bench-pack --pack packs/v5-dn3-epoch0 --batch-log2 24 --device (pc1-amd-v5-bench.ps1); src/bench.cu with nvcc. +Intel: not measured tonight (7 October 2026); no Arc B580 sits on PC 1 or PC 2 and the only Arc path needs a driver click, which no PC job +may raise; the card's holder and a click-free driver path are owed. The OpenCL worker takes the Intel device by index the same way. +R +( cd "$S" && find . -type f ! -name SHA256SUMS | sort | xargs sha256sum > SHA256SUMS ) +mkdir -p /srv/artefacts/packs +python3 - "$S" "$ZIP" <<'PY' +import os, sys, zipfile +stage, out = sys.argv[1], sys.argv[2] +with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as z: + for dp, dn, fn in os.walk(stage): + dn.sort() + for f in sorted(fn): + p = os.path.join(dp, f); rel = os.path.relpath(p, stage) + zi = zipfile.ZipInfo(rel, date_time=(2026, 10, 7, 0, 0, 0)); zi.compress_type = zipfile.ZIP_DEFLATED + zi.external_attr = (0o755 if rel.startswith('bin/') or rel.endswith('.sh') else 0o644) << 16 + with open(p, 'rb') as fh: z.writestr(zi, fh.read()) +PY +sha256sum "$ZIP" | cut -c1-64 > "$ZIP.sha256" +echo "KIT $ZIP bytes $(stat -c %s "$ZIP") files $(python3 -c "import zipfile,sys; print(len(zipfile.ZipFile(sys.argv[1]).namelist()))" "$ZIP")" +echo "KIT sha256 $(cat "$ZIP.sha256")" +for b in "$S"/bin/linux/* "$S"/bin/windows/*; do echo "BIN $(basename "$(dirname "$b")")/$(basename "$b") $(stat -c %s "$b") bytes sha256 $(sha256sum "$b" | cut -c1-64)"; done +finish +exit 0 diff --git a/tools/class-v5/pc1-v6-bench.ps1 b/tools/class-v5/pc1-v6-bench.ps1 new file mode 100644 index 000000000..84f8349cd --- /dev/null +++ b/tools/class-v5/pc1-v6-bench.ps1 @@ -0,0 +1,95 @@ +# Class v6 kit (8 October 2026, the coordinator's order): the amd job of the v5 script with the class v6 packs: hl-v6-foldrw (the +# known-failed partner; expected fingerprint 6ce3dc344a613500, the Mac's Metal and Apple OpenCL reading) and hl-v6-all (the +# all-together pack, id 9d40978601a7df2a; with the second zip its OpenCL text is the window's real text and the expected +# fingerprint is the CUDA reference e8f4f3289c6ee1fc; the first zip's stub refused it, that refusal line being that run's record). +# Kit fetch job fetch-class-v6-kit-20261008 = the second zip (packs-class-v6-.zip named in the job's publish line). +# Class v5 kit bench on PC 1's RX 9070 XT (7 October 2026, docs/design/class-v5-stored-state.md section 13, "the kit: +# OpenCL (AMD)"): the kit's igneum-worker-opencl.exe (THIS tree's proto-opencl/host.c with the class v5 leaf upload: +# igneum_build(ds, cache, leaves, nLeaves, nItems), the leaves of leaves.bin checked against the pack's FNV-1a 64 first) +# runs --bench-pack on the first class v5 pack, proto-cuda/packs-ca3-v5/v5-dn3-epoch0 (program id e5a4ac5978462156, 11 +# leaves under state root 7e37a9fb..., cache FNV 7334fa46e5d972eb), on the gfx1201 device. Expected: the self-test +# PASS line (cache head, last line and FNV; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes) and the +# fingerprint of the 2^24 outputs at base nonce 0 equal to the Metal reading, 6ce3dc344a613500 (the M5 Max, 7 October +# 2026, 18:07:09Z). The v4-genesis control pack runs after it so the run has a known class v4 row beside the v5 row. +# Beside the miners (a correctness and fingerprint gate; the rate row is labelled loaded when the card mines): no card is +# switched, nothing is posted to the installed app, nothing is built on the PC. Published by the Counter ASIC coordinator +# only (the PC 1 queue is its); the kit is fetch job $kitId. Every result line starts with RESULT; SUMMARY {json} ends it. +# Post-review packs (8 October 2026, 20:4x UK, the third kit zip packs-class-v6-20261008T194045Z.zip, sha256 735e1411...): the packs are +# generator 6 (class v6; ids hl-v6-all 4de7b836cc40a4ea, hl-v6-foldrw d7eba30115d26dd4) and the expected fingerprints are the +# Mac's Metal and Apple OpenCL reading of them (all pack e8f4f3289c6ee1fc, partner 6ce3dc344a613500); the generator 5 zips' values +# (59e6708e46f1e87c and 5a6ad122a71a888f) belong to the superseded packs and no longer match by construction. +$ErrorActionPreference = 'Continue' +$jobName = 'pc1-v6-bench' +$kitId = $env:IGNEUM_V5_KIT_ID; if (-not $kitId) { $kitId = 'fetch-class-v6-kit-20261008' } +$expected = '6ce3dc344a613500' +$started = Get-Date +function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') } +function Summary([string] $status, [hashtable] $extra) { + $o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) } + foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] } + 'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 4) +} +"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION expected_fingerprint=$expected" +$jobs = Split-Path $env:IGNEUM_JOB_DIR +$kit = Join-Path $jobs $kitId +if (-not (Test-Path $kit)) { "RESULT error kit missing at $kit (the fetch job $kitId runs first; republish it after any app update)"; Summary 'failed' @{ error = 'kit missing' }; exit 2 } +$exe = Join-Path $kit 'bin\windows\igneum-worker-opencl.exe' +$packs = Join-Path $kit 'packs' +if (-not (Test-Path $exe)) { "RESULT error worker missing at $exe"; Summary 'failed' @{ error = 'worker missing' }; exit 2 } +"RESULT worker kit $exe sha256 $((Get-FileHash -Algorithm SHA256 $exe).Hash.ToLower()) bytes $((Get-Item $exe).Length)" +foreach ($pk in @('hl-v6-foldrw', 'hl-v6-all')) { + $d = Join-Path $packs $pk + if (-not (Test-Path (Join-Path $d 'kernel_bound.cl'))) { "RESULT error pack $pk missing at $d"; Summary 'failed' @{ error = "pack $pk missing" }; exit 2 } + "RESULT pack $pk kernel_bound.cl sha256 $((Get-FileHash -Algorithm SHA256 (Join-Path $d 'kernel_bound.cl')).Hash.ToLower()) program.h sha256 $((Get-FileHash -Algorithm SHA256 (Join-Path $d 'program.h')).Hash.ToLower())" +} +# the class v6 kit's packs each carry their own leaves.bin (the class v5 script checked one pack by name; the 18:01 UK run on +# PC 1 stopped on that name before the device list) +foreach ($lp in @('hl-v6-foldrw', 'hl-v6-all')) { + $leaves = Join-Path $packs ($lp + '\leaves.bin') + if (-not (Test-Path $leaves)) { "RESULT error leaves.bin missing at $leaves (a class v5 pack without its leaves builds nothing)"; Summary 'failed' @{ error = 'leaves missing'; pack = $lp }; exit 2 } + "RESULT leaves $lp $((Get-Item $leaves).Length) bytes sha256 $((Get-FileHash -Algorithm SHA256 $leaves).Hash.ToLower())" +} +# the OpenCL device index of the 9070 XT (the installed worker's list when present: the app's own indices) +$inst = @("$env:LOCALAPPDATA\Programs\Igneum Miner", "$env:ProgramFiles\Igneum Miner") | Where-Object { Test-Path (Join-Path $_ 'igneum-app.exe') } | Select-Object -First 1 +$listExe = $exe +if ($inst -and (Test-Path (Join-Path $inst 'igneum-worker-opencl.exe'))) { $listExe = Join-Path $inst 'igneum-worker-opencl.exe' } +$list = @(& $listExe --list 2>&1 | ForEach-Object { "$_" }) +$list | ForEach-Object { "RESULT list $_" } +$dev = $null +foreach ($l in $list) { if ($l -match '^\s*\[(\d+)\].*gfx1102' -and $l -notmatch 'dup') { $dev = [int]$Matches[1]; break } } +if ($null -eq $dev) { "RESULT error no gfx1102 device in --list (the RX 7600 is off the bus: the AMD class v6 row stays OWED)"; Summary 'failed' @{ error = 'no gfx1102' }; exit 2 } +"RESULT device $dev gfx1102 (list from $listExe)" +function Workers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" }) } +$w = @(Workers) +$loaded = ($w | Where-Object { $_ -match "igneum-worker-opencl.*--device\s+$dev(\s|$)" }).Count -gt 0 +"RESULT workers_before $(Stamp) $($w -join ' ')" +$state = if ($loaded) { 'loaded' } else { 'quiet' } +"RESULT context card_state=$state (a fingerprint gate: the load changes the rate row, never the bytes)" +$rows = @{} +$fpOk = $false +foreach ($pk in @('hl-v6-foldrw', 'hl-v6-all')) { + $d = Join-Path $packs $pk + $t0 = Get-Date + "RESULT bench $pk start $(Stamp) cmd=igneum-worker-opencl.exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches 5" + $out = @(& $exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches 5 2>&1 | ForEach-Object { "$_" }) + $code = $LASTEXITCODE + $secs = [int]((Get-Date) - $t0).TotalSeconds + foreach ($l in $out) { if ($l -match '^(pack |class v5|RESULT |FAIL|error|warm-up)') { "RESULT bench $pk out $l" } } + $res = $out | Where-Object { $_ -match '^RESULT ' } | Select-Object -Last 1 + $fp = ''; $mhs = ''; $check = '' + if ($res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] } + if ($res -match 'mhs=([0-9.]+)') { $mhs = $Matches[1] } + if ($res -match 'check=(\w+)') { $check = $Matches[1] } + $rows[$pk] = [ordered]@{ exit = $code; seconds = $secs; fingerprint = $fp; mhs = $mhs; check = $check; card_state = $state } + if ($pk -eq 'hl-v6-foldrw') { + $fpOk = ($fp -eq $expected -and $check -eq 'PASS') + "RESULT partner fingerprint=$fp expected=$expected match=$fpOk check=$check mhs=$mhs card_state=$state exit=$code seconds=$secs $(Stamp)" + } else { + # the all pack: with the window's OpenCL text (the second zip) a fingerprint, expected the CUDA reference e8f4f3289c6ee1fc + $allOk = ($fp -eq 'e8f4f3289c6ee1fc' -and $check -eq 'PASS') + "RESULT all-pack fingerprint=$fp expected=e8f4f3289c6ee1fc match=$allOk check=$check mhs=$mhs card_state=$state exit=$code seconds=$secs $(Stamp)" + } +} +"RESULT workers_after $(Stamp) $((@(Workers)) -join ' ')" +Summary $(if ($fpOk) { 'done' } else { 'failed' }) @{ expected = $expected; partner = $rows['hl-v6-foldrw']; all_pack = $rows['hl-v6-all']; device = $dev; kit = $kitId } +exit $(if ($fpOk) { 0 } else { 1 })