igneum/igneum-pow
igneum-labs c3d32437cd Counter ASIC 3.0 gates (hash): class v4 sub-version 3, second commit (AP-F8-1): the shared-operand rule in the draw and (a'), rule (c'') the distinct-index ratio at 0.98 over 2^20, the known-failed test on the chain's candidates
The shared-operand rule (or-then-xor, or-then-sub, xor-then-or on one operand is a mask) in the draw's source rule and in the acceptance's (a') pass: p23's chain attempt 1 (id d65122675f16a1c7) now draws site 7 from r5 and passes. Rule (c''): over 4,096 units (2^20 evaluations per site, the shadow executed) every load site's distinct word indices against the uniform expectation on its window must reach 0.98, the last test of the chosen candidate; the floor sits 0.015 from the clean minimum (0.9960) and from the strong failing maximum (p56 0.9654). The staged 2^24 pass was not taken: at 2^24 the clean p44 and p52 (0.9612, 0.9613) read the weak four's value (0.9181 to 0.9630), so no 2^24 floor separates them. Open tail: p4, p8, p10, p34 (0.9927 to 0.9963 at 2^20), unattributed and chased. The (B) bound stays unwired. The test class_v4_distinct_ratio_rejects_the_low_entropy_band pins p15 (attempt 3), p18 (2), p19 (0), p56 (2) refused and p23's attempt 1 passing, its r6 variant refused by (a') and by the ratio (0.836). The stream is unchanged: re-export diff 0 on the eight packs, id a785001687d8688a, PROGRAM_SUBVERSION_V4 stays 3, fingerprints and zip sha256 stand. Cost: one 2^20 pass per chosen candidate, 2.1 to 2.2 s on one box-2 core.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 14:44:55 +00:00
..
examples Counter ASIC 3.0 item 2: the per-day item-derivation program (class dr736), its interpreter, emitter and packs 2026-10-06 07:41:23 +00:00
src Counter ASIC 3.0 gates (hash): class v4 sub-version 3, second commit (AP-F8-1): the shared-operand rule in the draw and (a'), rule (c'') the distinct-index ratio at 0.98 over 2^20, the known-failed test on the chain's candidates 2026-10-07 14:44:55 +00:00
tests Counter ASIC 3.0 gates (hash): class v4 sub-version 3, first commit (AP-F8-3; main's word: 0.3.21 ships byte 5, sub-version 3 is 0.3.22's). The acceptance executes the latency-shadow block as the hash does: run_unit runs the block after instruction 63 of every iteration, reps times with the iteration's sel (verify.rs); until now it ran the 64 base instructions only, so every dynamic acceptance test on a class v4 program judged a program the chain never hashes, which is the whole residual class behind sub-version 2's 8 of 64 gate failures. The test acceptance_executes_the_shadow_block_as_the_verifier_does pins the acceptance's execution to verify.rs on the devnet epoch-0 program and the six test eras (equal output bit counts over the 64 units; different with the shadow stripped). PROGRAM_SUBVERSION_V4 = 3 (a new verdict is a new stream). The seven gate packs re-exported: the devnet epoch-0 seed still accepts at attempt 1, so its program and fingerprint are sub-version 2's (e370fb2080b7dbb1) under the new id a785001687d8688a (must-differ: c120d7963abdcd96, 1a4230699a6b9c60, a788661687db4bb3); the seven 256-block ladder packs re-exported. The ledger entry carries AP-F8-2's exhaustion half as FIXED-AND-PASSED at fbb00320 (0 of 10^6, max attempt 35, r = 0.67), AP-F8-3, and p23's localisation (site 7 reads r6 = (mulhi | r4) ^ r4 = r6 & ~r4; 0.84 of uniform distinct indices at 2^20, 0.55 at 2^24, reproduced in the acceptance's own execution); the second commit, a per-site distinct-index ratio, is held 2026-10-07 14:20:02 +00:00
.gitignore igneum-pow: Rust crate bit-exact with proto-metal (seed, generator, memhard, verifier, emitters) 2026-10-03 16:53:18 +00:00
Cargo.lock Lottery hash: generator version 2 (16 load slots, fresh sources, acceptance rule), every vector re-cut, packs regenerated, three workers re-checked, 20,000-program census 2026-10-04 07:52:40 +00:00
Cargo.toml Lottery hash: generator version 2 (16 load slots, fresh sources, acceptance rule), every vector re-cut, packs regenerated, three workers re-checked, 20,000-program census 2026-10-04 07:52:40 +00:00
README.md Lottery hash: generator version 2 (16 load slots, fresh sources, acceptance rule), every vector re-cut, packs regenerated, three workers re-checked, 20,000-program census 2026-10-04 07:52:40 +00:00
rustfmt.toml igneum-pow: Rust crate bit-exact with proto-metal (seed, generator, memhard, verifier, emitters) 2026-10-03 16:53:18 +00:00

igneum-pow

The Igneum lottery hash in Rust: the generator, the acceptance rule, the memory-hard dataset, the CPU verifier and the kernel emitters. This is the crate the rusty-kaspa fork calls (docs/fork-map.md, rows a1 to a3) and, since 4 October 2026, the source of every program pack in proto-cuda/packs/. No dependency outside the standard library; serde_json is a dev-dependency for reading the packs in the tests.

Dates: 3 October 2026 (crate, bit-exact with the Swift prototype), 4 October 2026 (generator version 2 and the acceptance rule; every vector re-cut). Toolchain: rustc 1.99.0 via rustup (the Homebrew 1.69 on PATH is too old; use ~/.cargo/bin/cargo). Crate version 0.2.0.

Modules

Module What it is Swift namesake
seed 32-byte seed words from bytes (FNV-1a 64, four salts, finalised); seed_words_from_bytes is the boundary where the chain feeds the epoch seed; SplitMix64 seedWordsBytes, SplitMix64
generator version 2: 16 load slots drawn first from instructions 1..63, fresh-source loads, the other 48 ops from the ten non-load weights; attempts k = 0, 1, ... of a seed; the program id. The retired version 1 generator stays as generate_v1 for the census and the lever measurements generateProgramV2, candidateProgram, generateProgramV1
accept the acceptance rule of spec 01 section 1.4.6: two static tests and the 64-unit dynamic test on the seed-keyed closed-form dataset acceptProgram
memhard 256 MiB cache (2^16 chains of 64 ChaCha12 blocks), mixer parameters, 8-round item derivation with the 32 lanes interleaved, MemhardCpu::fetch cpuFillCache, MixParams, deriveItems, MemhardCPU
verify the 32-lane warp interpreter, DatasetMode::{ClosedForm, MemoryHard}, Epoch, hash_warp, verify_block cpuWarp, DatasetSource
emit Metal, CUDA and OpenCL source, program.h, memhard.h, vectors.h, program.json, vectors.json, the header-bound kernels, export_pack generateMSL, generateCUDA, generateOpenCL, exportPack
bind header binding (spec 01 section 1.6): init words from "igneum-block/" || H || nonce_hi_le32, bound hash API on Epoch, the 256-bit pow mapping, the interim day seed bytes blockInitWords

Generator version 2 (4 October 2026)

Adopted from docs/analysis/weak-program-census-2026-10-03.md (ledger M5 and M6). Three parts, all in this crate and mirrored in proto-metal/main.swift so the Metal worker derives the same program from the same seed:

Part Rule Where
G1, exact load count 16 load instructions per program, a uniform 16-subset of slots 1..63 drawn first by partial Fisher-Yates over the program stream; the other 48 ops from add 12, xor 10, mul 8, mad 8, shfl 8, rotl 7, sub 6, mulhi 6, rotr 6, or 4 (sum 75). 128 loads per hash, 4,096 items per unit generator::candidate_from_words
G2, fresh source a load's source is drawn from the registers other than dst written by an earlier instruction and not read by a load since, so no load repeats an earlier load's address in the hash same
R, acceptance (a) no load whose source is unwritten since the previous load from it, cyclically; (b) every register has an add, sub, xor, mad, shfl or load write; (c) 64 units at base nonces from SplitMix64(FNV-1a-64("igneum-accept/" || seed words LE)), init words = seed words, closed-form dataset dataset_elem(idx, S[0], S[1]) at 2^28 words: no constant register bit, no lane-constant load site in any unit, fewer than 164 saturated final values, every output bit within 136 of 1,024, distinct addresses above 245,760 over the 2,048 hashes accept::check
Attempts a rejected candidate is replaced by seed_words_from_bytes(seed || k_le32) for k = 1, 2, ...; 32 consecutive rejections are a consensus fault (probability below 2^-136 at the measured 5 percent rate) generator::generate_from_seed_bytes
Program id FNV-1a-64("igneum-program/" || 2_le32 || seed words LE || attempt_le32), written into program.json and program.h with the generator version and the attempt, so a version 1 pack or another attempt can never pass for the current program Program::program_id

Measured on this crate (igneum-pow accept): igneum-genesis attempt 0 accepted, program id bcc1248b10cc90f2, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1, 128.000 distinct addresses per hash; igneum-hourly attempt 0, id a4c4d00961c855df; seeds igneum-census-2026-10-03/22, /37 and /51 have attempt 0 rejected ((b) r7 without an injecting write; (c) 119.74 distinct addresses; (b) r4) and attempt 1 accepted (ids 22ed0609d079f4cf, 947705cc4eb1df0a, 9869afcc028bf9f1); those three are the conformance vectors for the attempt rule. The rule costs 1.3 to 3.4 ms per seed on one core. The 20,000-program census under this generator is in docs/bench-log.md (4 October 2026 entry).

The API the fork calls

use igneum_pow::{Epoch, DatasetMode};

// Once per epoch and day: derives the accepted program and fills the 256 MiB cache (about 0.2 s on one core).
let epoch = Epoch::memory_hard("igneum-genesis", "2026-10-03");

let h: u64 = epoch.hash(nonce);                 // one nonce (computes its aligned 32-nonce warp)
let w: [u64; 32] = epoch.hash_warp(base_nonce); // one warp
let ok: bool = epoch.verify_block(nonce, target_u64);

// Miner programs for the epoch: the pack every worker compiles.
let pack = igneum_pow::emit::export_pack(&epoch, "2026-10-03", "igneum node");
pack.write_to(std::path::Path::new("out"))?;   // kernel.cu, kernel.cl, program.metal, memhard.h, ...

The header-bound form (what the chain uses)

The pack form above initialises the lane registers from the program's own seed words, so one nonce has one hash per epoch whatever block is mined. On the chain the init words commit to the block (spec 01 section 1.6, src/bind.rs):

H        = header hash with the nonce field zeroed, every other field as mined
           (rusty-kaspa hash_override_nonce_time(header, 0, header.timestamp))
nonce    = 64 bits; lane nonce n = low 32 bits; nonce_hi = high 32 bits
I        = seed_words_from_bytes("igneum-block/" || H || nonce_hi_le32)      (49 bytes in)
hash     = interpret(program, I, n)                                          (section 1.7 of the spec)
pow256   = hash in the top 64 bits, low 192 bits zero (little-endian bytes 24..32)
valid    = pow256 <= target256, which is exactly hash <= target256 >> 192

H keeps the timestamp (Kaspa zeroes it in the pre-PoW hash and absorbs it in cSHAKE afterwards; the lane hash has no afterwards, so a nonce would otherwise be reusable across timestamps). The interim day seed is "igneum-day/" || day_le64 with day = timestamp_ms / 86,400,000. The epoch seed bytes are the 32 bytes of the epoch block hash (devnet); the program is generate_from_seed_bytes(epoch_seed), attempts included.

use igneum_pow::{bind, Epoch};

let epoch = Epoch::from_seed_bytes(epoch_hash.as_bytes(), &bind::day_bytes(day), "label");
let lane: u64 = epoch.hash_bound(&prehash, nonce);               // one 64-bit nonce
let warp: [u64; 32] = epoch.hash_warp_bound(&prehash, nonce);    // its aligned 32-nonce group
let init = bind::block_init_words(&prehash, nonce);              // what a GPU kernel takes as its argument
let same = epoch.hash_warp_init(&init, nonce as u32 & !31);     // == warp
let pow: [u8; 32] = epoch.pow_bound(&prehash, nonce);
let ok = epoch.verify_block_bound(&prehash, nonce, bind::target64_from_le256(&target_le));

On the GPU the init words are a kernel argument: igneum_hash_bound in program_bound.metal takes constant uint* initw [[buffer(3)]], kernel_bound.cu takes IgneumInitWords iw by value, kernel_bound.cl an initw buffer. All three differ from igneum_hash only in the kernel name, the argument and the eight init lines.

Bound vectors (seed igneum-genesis, day 2026-10-03, memory-hard, 2^28 words, generator v2; igneum-pow hash-bound):

H nonce hash_bound
32 zero bytes 0 746c567b090acf6a
32 zero bytes 1 45a619f860880c73
32 zero bytes 31 9aa495e43dedbfe6
32 zero bytes 4096 2e6ffd7624d3cba2
32 zero bytes 4294967296 (1 << 32) 38a5cea1fb01431a
bytes 00 01 02 .. 1f 0 2a79c5e4797bf6aa
bytes 00 01 02 .. 1f 4294967301 ((1 << 32) + 5) a243e0c61aa1b82e
bytes 00 01 02 .. 1f 18446744073709551615 (u64::MAX) 9c7bbfbd064fe1a4

Init words for H = 32 zero bytes, nonce 0: 595a8f8a 37647e95 faadade1 cbbcf2a4 54f7cc13 f6851b5e 8c68ca04 7991ea9c (unchanged by v2: the binding does not depend on the program). The eight are pinned in bind::tests::bound_vectors. The version 1 bound vectors of 3 October 2026 are retired.

Epoch is Send + Sync; build one and share it. DatasetMode::ClosedForm is the interpreter regression dataset of the packs igneum-genesis and igneum-hourly and is not memory-hard.

CLI

cargo build --release
./target/release/igneum-pow bench  --seed igneum-genesis [--warps 20] [--closed-form] [--day 2026-10-03]
./target/release/igneum-pow export --seed igneum-genesis --out <dir> [--closed-form]
./target/release/igneum-pow export --epoch-hex <64 hex> --day-hex <hex> --out <dir>     the chain's byte seeds
./target/release/igneum-pow hash   --seed igneum-genesis --nonce 4103
./target/release/igneum-pow hash-bound --seed igneum-genesis --prehash <64 hex> --nonce <u64>
./target/release/igneum-pow accept --seed igneum-census-2026-10-03/22      every candidate with its verdict
./target/release/igneum-pow show   --seed igneum-genesis                   the accepted program, one line per instruction

The packs were regenerated on 4 October 2026 with exactly these commands:

igneum-pow export --closed-form --seed igneum-genesis --out ../proto-cuda/packs/igneum-genesis
igneum-pow export --closed-form --seed igneum-hourly  --out ../proto-cuda/packs/igneum-hourly
igneum-pow export --seed igneum-genesis --out ../proto-cuda/packs/igneum-genesis-mh
igneum-pow export --epoch-hex edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 \
                  --day-hex 69676e65756d2d6461792ffa50000000000000 --out ../proto-cuda/packs/igneum-devnet-v4-epoch0

The last is the chain's own derivation for devnet v4 epoch 0: the devnet genesis hash as the epoch seed and bind::day_bytes(20730) (2026-10-04) as the day bytes.

Tests

cargo test --release (39 tests, about 2 s after compile; the dev profile is optimised so the cache fill is quick):

Check Pack Result
program.json instruction by instruction from seed_bytes, generator 2, attempt, program id, op mix, 128 loads, acceptance all four match
Mixer parameters (key, rot, mul, rc) igneum-genesis-mh, igneum-devnet-v4-epoch0 match
Cache head, last line, FNV-1a 64 (48c4f5bf24166b2e for day 2026-10-03, 448274a57f508cbc for day bytes 20730) the two memory-hard packs match
Dataset head (16), [MASK], 64 sampled words all four match
96 hash vectors (3 warps x 32 lanes) all four 96/96 each
kernel.cu, kernel_bound.cu, program.metal, program_bound.metal, kernel.cl, kernel_bound.cl, program.h, program.json byte-identical; 16 masked loads per kernel all four identical
memhard.h, memhard.metal byte-identical the two memory-hard packs identical
vectors.json, vectors.h byte-identical; no stale file in any pack directory all four identical
bound vectors (8), bound warp == bound single, H and nonce_hi enter the hash igneum-genesis-mh pass
the devnet pack equals Epoch::from_seed_bytes(genesis, day_bytes(20730)) igneum-devnet-v4-epoch0 pass
acceptance: instrumented interpreter == hash_warp, cyclic stale-load detection, injecting-write detection, rejection under 12.5 percent and distinct loads above 127 on 400 census seeds, version 1 programs mostly rejected unit tests pass
generator: 16 loads, none at instruction 0, contract on 200 candidates; fresh sources; attempt words; program ids separate versions and attempts unit tests pass

Measured, Apple M5 Max, one core, release build

Step 3 October 2026 (v1, 104 loads) 4 October 2026 (v2, 128 loads, 4,096 items)
Cache fill, 256 MiB 175 to 181 ms 179 ms
CPU verify per warp, igneum-genesis, avg of 20 0.441 ms (3,328 items) 0.631 ms
Cold single warps (bases 0, 4096, 1000000) 0.41 to 0.87 ms 0.67 to 0.81 ms
Acceptance rule per candidate 1.3 to 3.4 ms
Closed form, igneum-genesis 0.002 ms 0.002 ms

The 10 ms gate holds with a margin of about 16x steady on this core. Every verified unit now derives exactly 4,096 items, the design bound of spec section 1.11.

Not done here

  • No GPU. The vectors tie this crate to the Metal, CUDA and OpenCL results through the packs; nothing here runs a kernel.
  • The epoch seed enters at Epoch::from_seed_bytes (the epoch block hash on devnet); the VDF output replaces it there.
  • The dynamic acceptance test is specified on the closed-form dataset at 2^28 words; if the prototype mask ever changes, the rule's constant stays at 2^28.