igneum/docs/plans/mixer-x4.md

19 KiB

Mixer x4 and the cache growth rule: the class v3 dataset construction

5 October 2026 (night). Counter ASIC 2.0, layer 6 (option C) and ledger M16's lever, decided by the coordinator under the project lead's delegation at 22:00 UTC (docs/plans/counter-asic-2-status.md, "22:00 decided"; the project lead confirms for the public testnet genesis). Branch ca2-mixer. Worker: ca2-mixer (cryptographer's lane).

What this changes, in one line: under program class v3 every mixer application of the dataset item derivation becomes four applications with distinct round keys, the eight dependent cache reads per item stay eight, and the 256 MiB cache doubles on the days the dataset doubles (years 4 and 12). Version 2 is byte-identical: the two pinned packs re-export without a changed byte (section 5).

1. Why this form

The recompute attacker of docs/analysis/m16-recompute-attacker-2026-10-05.md holds the 256 MiB cache on a die and derives every dataset word instead of reading it: 128 items per hash at about 1,170 integer operations and 8 dependent cache reads each. Its cost is linear in operations per item; the honest miner pays the mixer once a day in the dataset build and never per hash; the verifier pays it per item it checks. The multiplier m is the one parameter that moves the attacker and leaves the honest hash rate untouched.

Two shapes give the attacker 4x the operations:

Shape Mixer applications per item Dependent cache reads per item What grows for the verifier What grows for the chip What grows for the honest build
A, chosen: m = 4 applications per round, 8 rounds 36 8 the ALU part only; the latency part (8 dependent misses per item) unchanged integer operations 4x; SRAM bandwidth unchanged (1,024 reads per hash) 4x the mixer arithmetic, same reads
B, alternative: 32 rounds of one application and one read 33 32 both parts: 4x the dependent misses per item, so about 4x the latency-bound time (spec 1.11: about 8 x 100 ns per item in series without interleaving) integer operations 3.7x and SRAM bandwidth 4x (4,096 reads per hash, 60 TB/s to match one 5090 at the M16 rate) 4x the reads too; the GPU build becomes latency-bound at 4x the dependent line fetches

Shape A is chosen because the verifier's latency part is the part the 10 ms gate protects (section 1.11: the distinct items of a unit are derived with their chains interleaved so the 8 misses of each item overlap across up to 32 items; shape B would make that 32 misses deep). Shape B is implemented nowhere; the rule in the brief: if shape A's measured verifier time exceeds 4.8 ms per warp on one M5 Max core, measure both and recommend. Section 6 has the measurement; it is under that bound, so B stays unimplemented.

2. Spec text (replaces 1.8.5 and 1.13.3 under class v3; v2 text unchanged)

1.8.5 Item derivation and dataset mapping

Item t (16 words) under mixer multiplier m (m = 1 for program class v2, m = 4 for class v3; a class parameter, LoadClass::mixer_mult):

s[i]     = K[i]                       for i in 0..7
s[8 + i] = t * MUL[i] + RC[i]         for i in 0..7
for r in 0..7:
    for j in 0..m-1:
        s = M(s, rk = (r * m + j + 1) * 0x9E3779B9)
    a = s[0] AND (2^(C - 4) - 1)      cache line index, 2^(C - 4) lines of a 2^C-word cache
    s[i] = s[i] XOR cache[line a][i]  for i in 0..15
for j in 0..m-1:
    s = M(s, rk = (8 * m + j + 1) * 0x9E3779B9)
item(t) = s

M(s, rk) is the mixer of 1.8.4 with round key rk; under m = 1 the keys are (r + 1) * 0x9E3779B9 and 9 * 0x9E3779B9, the version 2 text exactly. The round keys of the 9 m applications are the first 9 m values of the version 2 key sequence, all distinct (the sequence is k * 0x9E3779B9 for k = 1 .. 9 m, and 0x9E3779B9 is odd, so no two of the first 2^32 keys coincide). Eight dependent cache reads per item at every m (ITEM_ROUNDS = 8, prototype value): the address of read r depends on every earlier read. 9 m mixer applications, 36 under class v3, about 4,700 integer operations per item (130 per application, 1.8.4). dataset[w] = item(w >> 4)[w AND 15]. A dataset of 2^D words is the prefix of items 0 .. 2^(D-4) - 1, so an item has the same value at every dataset size; and a cache of 2^C words is the prefix of segments of every larger cache (1.8.3 fills segments independently of the cache size), but an item's value depends on C through the line mask, so the item changes on the day the cache doubles.

Source: igneum-pow/src/memhard.rs (derive_items, round_key_mult, Shape), the emitted mh_item of memhard.h, memhard.metal and kernel.cl (emit.rs, emit_memhard_core: the m loop is emitted only for m > 1, so every version 2 pack keeps its text).

1.13.3 Dataset growth (class v3: option (b) with the cache tied to it, "option C")

Designed: 2 GiB at genesis plus 0.5 GiB per year. The linear schedule in bytes, G x (1 + 86,400 d / (4 x 31,536,000)) = G x (1 + d / 1,460) for the genesis size G and the chain day d (DAA days since genesis, section 1.12), doubles at day 1,460 (year 4), quadruples at day 4,380 (year 12), reaches 8x at day 10,220 (year 28). Rule (Designed, decided 5 October 2026 for class v3):

doublings(d)        = floor(log2(1 + d / 1460))        integer division, then integer log2
dataset_words(d)    = 2^(D_0 + doublings(d))            D_0 = 29 designed (2 GiB), 28 on the devnet (1 GiB); capped at 32
cache_words(d)      = 2^(26 + doublings(d))             256 MiB, 512 MiB from year 4, 1 GiB from year 12

Power-of-two sizes only (option (b)), so every load keeps the src AND MASK form of 1.14 item 2 and the cache line index keeps s[0] AND mask. The cache doubles exactly when the dataset doubles ("option C", docs/analysis/sram-mirror.md section 7): the cache's job is to stay above any GPU's last-level cache and that needs growth; the recompute attacker is priced by the mixer, not by the cache (section 7 below).

d is day_index(header.timestamp) - day_index(genesis.timestamp) with day_index = timestamp_ms / 86,400,000 (bind::day_index, the interim day rule), clamped at 0 (memhard::days_since_genesis). Under class v2 nothing grows: the cache is 2^26 words and the dataset the genesis size on every day.

Chain day d Years doublings Cache words Cache Dataset words (devnet D_0 = 28) Dataset (designed D_0 = 29) Verifier cache fill, one M5 Max core (measured at 256 MiB, section 6, scaled linearly)
0 to 1,459 0 to 4 0 2^26 256 MiB 2^28 (1 GiB) 2 GiB 0.18 s
1,460 to 4,379 4 to 12 1 2^27 512 MiB 2^29 (2 GiB) 4 GiB 0.36 s
4,380 to 10,219 12 to 28 2 2^28 1 GiB 2^30 (4 GiB) 8 GiB 0.72 s
10,220 to 21,899 28 to 60 3 2^29 2 GiB 2^31 (8 GiB) 16 GiB 1.4 s
21,900 and on 60 and on 4 2^30 4 GiB 2^32 (16 GiB, the index cap) 2^32 words, the cap 2.9 s

Test: memhard::tests::growth_schedule_table pins every row and the day before each step. The devnet pack igneum-devnet-v4-epoch0 is day 20,730 of the Unix count against genesis day 20,729, d = 1, so every existing size and vector stands.

Consequences for the tiers (the rule of 5 October): a verifier (any node, any pool core) holds 512 MiB from year 4 and 1 GiB from year 12, and fills it once a day in under a second on one 2026 core (the table); a miner's card holds the dataset, 4 GiB from year 4 and 8 GiB from year 12 on the designed schedule, so an 8 GB card mines until year 12 and a 16 GB card until year 28 (the cache is not in the card's working set at hash time: it is built, the dataset built from it, and dropped). Those dates are the design document's own schedule restated as steps; option (a) would have faded a 4 GiB card out in year 4 instead of year 4.

3. Interfaces

Item Where Note
LoadClass { mixer_mult: u8, growth: bool }, LoadClass::MX4 ("mx4"), with_mixer(m, growth), v2_loads(), takes_width_roll() generator.rs a class with v2 loads takes no width roll: its program stream is version 2's draw for draw, so the v3 program of a seed is the v2 program of that seed, only the dataset differs
Shape { mixer_mult, cache_log2_words }, Shape::for_class_day(class, d), MixParams.shape, Cache::fill_log2(key, log2), round_key_mult(r, j, m) memhard.rs Shape::V2 is version 2
growth_doublings(d), cache_log2_words(d), dataset_log2_words(D_0, d), days_since_genesis(day, genesis_day) memhard.rs the schedule, one function and its two sizes
DatasetSource::{new_shape, from_key_shape, shape}, Epoch::new_class_day, Epoch::from_seed_bytes_day(epoch, day, label, class, d, D_0) verify.rs the day-sized entries; the v2 entries are unchanged and build the v2 shape
IGNEUM_MIXER_MULT, IGNEUM_CLASS_MIXER_MULT, IGNEUM_CACHE_GROWTH in program.h; "mixer_mult", "cache_growth", the "item" string in program.json emit.rs written only for a class with m != 1 or growth, so v2 packs do not change
packfile.h mixerMult; packbench and the OpenCL host print the multiplier and the cache size the three hosts the kernels carry the construction in their text (one emitter, three dialects); the hosts size the cache from IGNEUM_CACHE_LOG2_WORDS already (packbench.swift line 56, host.cu line 65, host.c line 1966)
igneum-pow --class mx4 [--days d] on every command main.rs --days sizes the cache for a growth class

Under the ca2-v3 seam (ProgramClass::V3, V3_CLASS), the integration sets V3_CLASS = LoadClass::MX4; the chain's day-sized dataset needs the day index, so Epoch::chain_dataset(day, class) builds the genesis-size cache and a chain_dataset_day(day_bytes, class, d, D_0) beside it is the growth entry (section 9, owed to the node agent).

4. Vectors (class v3, proto-cuda/packs-ca2-mixer/)

Produced by igneum-pow export --program-class v3 (the Rust CPU interpreter, 5 October 2026, commit 66eeba3) and checked on the GPUs in section 6. The program of each pack is the version 2 program of the same seed instruction for instruction (tests/packs.rs, v3_packs_are_the_v2_seeds_under_mixer_x4); the cache is the version 2 cache (day 0 of the growth rule); the dataset words and the hashes are new. The 64 sampled indices are those of every pack (emit::sample_indices).

Pack mx4-genesis (seed igneum-genesis, day 2026-10-03, generator 3, class mx4, program id e323b9dcaf283a6f, 2^28 words, 2^26-word cache, cache FNV-1a 64 48c4f5bf24166b2e as under v2):

dataset words 0..15 (item 0):
  61ff2180 0d4c7e6c 2177d443 60df9025 cf8b2e10 63675bfb 25289e58 9c45dc42
  2d271c54 9652369b 2dd77508 5921392c 3afa60ee c640ad68 f2bb56ff cfa46438
dataset[0x0fffffff] = 5020180e
sampled words (the first 8 of the 64 in vectors.json):
  dataset[59471966] = de85726d   dataset[217795994] = 7cfc31c7   dataset[208353206] = d3cd5289   dataset[42483309] = 6ebbeef1
  dataset[172547758] = 9858413b   dataset[148076330] = e786f141   dataset[183853158] = 64f13833   dataset[214389424] = ca229d04
unit at base nonce 0, lanes 0..31:
  63acd2d273f475ba e929c78b34b80d4b 0b1011cb19982558 1457a0df5497aa11
  957d0f3bb71d98fb ac16901e6e6f6057 8ea1c6279f4b177a f28146e60bd08ba9
  fe5b8cfe87f8e65b 49f87240566ace62 6ef6d6b7bdea8e41 46d9c0dc29a97b9c
  111fe30128db9398 66dc39084f0946d4 8ee11bdfd35fecf2 2861fcfc75db6677
  31c7667d4bde8556 c5989c48858b4ce0 276395e734a9d30d 84217b41e91368ff
  3604861e34d9f697 9f51d8ee16bf3639 c89e47bafa84401c 7ae78c1f10b70e19
  0b8c947157a29a48 d67192e8cfb43842 05a4c6d182c8c675 188e2661f3263f2e
  a2df24238f7fea2e ed69ea7e13ad3a48 2d44ae509bab91b8 adad61931ea4fb70
unit at base nonce 4096: lane 0 edd508ac57e5699a, lane 1 4eefd56d526cdaeb, lane 31 8892f8604733b1e0
unit at base nonce 1000000: lane 0 8b3183778a49f59c, lane 1 1831b72a8797e895, lane 31 75eae55eba53a506

Pack mx4-devnet-epoch0 (seed the devnet genesis hash, day bytes of 2026-10-04, generator 3, class mx4, program id 73bcbfe8ccf988f1, 2^28 words, 2^26-word cache, cache FNV-1a 64 448274a57f508cbc as under v2):

dataset words 0..15 (item 0):
  afe80d67 b9fbd029 6c79f193 95139ad9 96310aff 4609f8b1 75279e63 28235be1
  47b17dcb 718e0ef2 a52588c8 a8bf49d5 19cf243e 5ec8905e a4851f66 af9cd9f3
dataset[0x0fffffff] = e6a99c7a
sampled words (the first 8 of the 64 in vectors.json):
  dataset[59471966] = 57642b58   dataset[217795994] = c279badd   dataset[208353206] = cbccbaad   dataset[42483309] = 32cce392
  dataset[172547758] = 71fdb4c6   dataset[148076330] = f5a268ce   dataset[183853158] = 3ca1d676   dataset[214389424] = 7977b03d
unit at base nonce 0, lanes 0..31:
  212c6442b51e87ae c374795c00839331 b6036a220a98f4b3 eb8b8013e637367b
  db5866e9b73930fd f3f3d01f46e90333 9d913991ab8ed428 7ccb1d8fa100a800
  3cf45ba44f09a0fe 91acf48ef1a63082 6ea46c69fb082f99 581f0218977a9d72
  9a4623a5c62ddf2d ab6eb5e768f0feb4 07b70bdccf8aca12 d666311ae5e4311e
  53114757d669f0a4 bd5d6ace87ce2ce4 fb712015e8189192 a32cec81103e134b
  83f3d18c3289c124 fe29f1984b132b3d c9ffcf4e3774497a ac99c9243dc63809
  d78a7e8217a32f3c ab81ad63d242fc31 0e4c30b7e00024af ce014289fff6778d
  64a1292e2a8b4a91 d5b8c90e681d7e3a 06078117673030fd 51bf77b280173930
unit at base nonce 4096: lane 0 3c797978566b5950, lane 1 7c759e60185b6411, lane 31 96a903eb9a0ca390
unit at base nonce 1000000: lane 0 d5a8da0568df8ee7, lane 1 68cb69c04208285a, lane 31 f8ca84a1a5d78cf5

5. The v2 path is byte-identical

cargo test --test packs regenerates every file of igneum-genesis-mh and igneum-devnet-v4-epoch0 from program.json and compares byte for byte (emitted_sources_match_all_packs, export_pack_matches_all_packs); section 6 also records a fresh igneum-pow export of both packs diffed against the checked-in directories.

6. Measurements

Machine: Apple M5 Max, 64 GiB, Darwin 25.6.0, 5 October 2026 (night), other agents' builds running beside every run; a timing row says which lock it ran under (measure is exclusive; run and build are not timings).

6.1 The v2 path, byte for byte (no lock needed)

igneum-pow export --seed igneum-genesis --day 2026-10-03 and igneum-pow export --epoch-hex edc4fa84...fb07 --day-hex 69676e65756d2d6461792ffa50000000000000 on commit 66eeba3, diff -r against proto-cuda/packs/igneum-genesis-mh and igneum-devnet-v4-epoch0: IDENTICAL, both (twelve files each). The crate tests regenerate the same files and compare them on every run (tests/packs.rs, 12 of 12 pass).

6.2 Bit-exactness of the class v3 construction on the GPUs (with-lock.sh run, 22:05 UTC)

packbench --pack <dir> --batches 1 --batch-log2 24 --group 256 (Metal, built from this branch) and igneum-bench-cl-igneum-genesis-mh --bench-pack --pack <dir> --batches 1 --batch-log2 24 (Apple OpenCL, built from this branch's host.c). Vectors are the Rust interpreter's; the fingerprint is FNV-1a 64 over the 2^24 outputs at base nonce 0.

Pack Harness Cache FNV-1a 64 Dataset head, word MASK, 64 samples Vectors standalone / in batch Fingerprint 2^24 MH/s (GPU time; not a measurement, the run lock)
mx4-genesis Metal 48c4f5bf24166b2e PASS PASS 3/3, 3/3 6f48d5a2aa0dbe5f 27.6
mx4-genesis Apple OpenCL PASS PASS 96 of 96 lanes 6f48d5a2aa0dbe5f 27.7 (wall)
mx4-devnet-epoch0 Metal 448274a57f508cbc PASS PASS 3/3, 3/3 73caaebb28e808fe 27.5
mx4-devnet-epoch0 Apple OpenCL PASS PASS 96 of 96 lanes 73caaebb28e808fe 27.6 (wall)

Reading: the Rust interpreter, Metal and Apple OpenCL agree on the v3 dataset (head, MASK word, 64 samples), on every vector lane and on the 2^24-output fingerprint of each pack; the hash rate is the v2 rate (27.7 MH/s on this card tonight, readwidth table), as it must be: the hash kernel only loads, the mixer is paid in the build.

Dataset build under the run lock (indicative only; the measured rows are in 6.4): Metal 30.2 ms GPU (mx4-genesis) and 21.7 ms GPU (mx4-devnet-epoch0) for 1 GiB; Apple OpenCL 55 and 56 ms wall. The readwidth entry's v2 figure on this card is 0.6 to 2.1 ms cache fill and a 1 GiB build the bench-log's memory-hard entry puts at 13 to 30 ms; the x4 build on the GPU is the row the measure lock will settle.

6.3 Soundness suite on the v3 construction (commit 66eeba3 and after; with-lock.sh build for cargo, run for the GPU)

Suite Command Result
Crate lib tests (memhard schedule table, the by-hand multiplied mixer, the seam, the generator) cargo test -j4 --release 44 of 44 pass
Pinned packs, v2 and v3 (tests/packs.rs: programs, ids, dataset words, 96 vectors per pack, every emitted file byte for byte, 16 masked loads per kernel, the v3 packs as the v2 seeds under mixer x4) same 12 of 12 pass
Scratch soundness tests of branch ca2-soundness (cherry-pick 0d8f745, one conflict in packbench.swift's RESULT line resolved by hand, era_bytes: None added to the edge program literal) cargo test -j4 --release --test scratch 7 of 7 pass: bijections, re-hit rates, 56 of 56 edge units, 42 of 42 emitted scr kernels, 200 scratch programs and 800 units on the CPU
v3 fuzz on the CPU (tests/mixer.rs): 200 programs through the seam, the contract on every instruction (each is the v2 program of its seed), 4 units each across the 32-bit range with one unit in the top 256 nonces, interpreted twice IGNEUM_MIXER_PACKS_OUT=<dir> cargo test -j4 --release --test mixer 200 of 200, 800 of 800 units; 200 packs written for the GPU runs (82 s with the three cache fills)
v3 stats beside v2 (tests/mixer.rs): 8,192 outputs per seed, bit balance, single-bit avalanche within the unit and across units, duplicates same igneum-genesis v3: avalanche 49.99 percent, worst bit z 1.92, 0 duplicates (v2: 49.87, z 2.25); igneum-genesis/stats1 v3: 49.97, z 3.09 (v2: 49.98, z 2.30)
v3 edge (tests/mixer.rs): items 0, 1, 2^28 - 1 and 2^32 - 1 by hand at m = 1, 2, 4, 8 on a 2^14-word cache; words 0, 15, 16, 17, MASK - 1, MASK through the interpreter's fetch path; the index wrap at MASK + 1 same pass
v3 determinism (tests/mixer.rs): two independent epochs, every vector and every emitted file equal, and equal to the pinned pack same pass
Metal and Apple OpenCL on the two pinned v3 packs section 6.2 3/3 standalone, 3/3 in batch, 96 of 96 lanes, dataset head, MASK word and 64 samples, one fingerprint per pack across both harnesses
Metal fuzz: the 200 packs, 4 units each standalone and the top-256 unit inside a 512-nonce batch at base 4,294,967,040; every tenth pack on Apple OpenCL as well packbench --pack <dir> --batches 1 --batch-log2 9 --batch-base 4294967040 (Metal), igneum-bench-cl-igneum-genesis-mh --bench-pack --pack <dir> --batches 1 --batch-log2 10 (Apple OpenCL), with-lock.sh run, 21:22 to 21:24 UTC Metal 200 of 200 packs PASS (800 of 800 standalone units, 200 of 200 inside the wrapping window, cache and dataset self-tests on every pack); Apple OpenCL 20 of 20 packs PASS (the three vectors.h units, the self-tests); a first run with a packbench built before the --batch-base cherry-pick reported 200 of 200 FAIL on an empty RESULT line and was read as such (the watcher rule), the harness rebuilt and the run repeated

6.4 Timings (with-lock.sh measure)

Owed until the measure lock frees.

7. The chip model

docs/analysis/chip-model-v3.md.

8. What is unverified

Filled at the end.

9. Owed

Filled at the end.