igneum/igneum-pow/README.md
igneum-labs fdcab858e3 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
igneum-pow 0.2.0: generator v2 draws exactly 16 load slots from instructions 1..63, a
load's source from the registers written earlier and not read by a load since, the other
48 ops from the ten non-load weights; accept.rs is spec 01 section 1.4.6 (static: no
stale load source, every register injected; dynamic: 64 units on the seed-keyed
closed-form dataset, no constant bit, no lane-constant site, under 164 saturated, bias
within 136 of 1024, distinct addresses above 245,760); a rejected candidate is replaced
by the next attempt of the seed (seed || k_le32), 32 a consensus fault. Packs carry the
generator version, attempt and program id. Version 1 kept as generate_v1 for the census.

Packs: igneum-genesis, igneum-hourly, igneum-genesis-mh regenerated by igneum-pow export;
new igneum-devnet-v4-epoch0 (devnet genesis hash, day bytes 20730). Checks: Rust 39 of
39 tests; Metal natively via the Swift port (export cross-check 3 of 3 warps, identical
programs and vectors on five seeds incl. three with attempt 1, fuzz 2,000 of 2,000);
CUDA emu 4 of 4 packs; OpenCL emu 2 packs x 2 configurations; Apple OpenCL 4 of 4 packs
at 27.9 Mhash/s. Census 20,000: 5.225 percent rejected, accepted distinct mean 127.887.

Spec 01 0.2 (1.4.2, 1.4.3, 1.4.6, 1.11, 1.15, 1.16, 1.17), igneum-pow README, the CUDA,
OpenCL and Metal test notes, bench-log entry, ledger M5 and M6 Fixed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 07:52:40 +00:00

13 KiB

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.