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>
181 lines
13 KiB
Markdown
181 lines
13 KiB
Markdown
# 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
|
|
|
|
```rust
|
|
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.
|
|
|
|
```rust
|
|
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.
|