class v5: ledger row M35 (the pool-ships-it objection answered with the state-size floor and the WAN line), the litepaper's 'The dataset is the chain' paragraph after the ladder's, the pinned-pack test for proto-cuda/packs-ca3-v5 (the v4 control pack's vectors agree with the v5 epoch on no lane; the hash kernel text equal, the build kernel and the leaves the difference), the design page's time line

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 08:17:01 +00:00
parent 41f055abc5
commit 661e800ce7
4 changed files with 112 additions and 0 deletions

View file

@ -11,6 +11,10 @@
| 08:33 | The canonical day stream written in the fork (`igneum/exec/src/day_stream.rs`) and run against node 1's exec snapshot on igneum-build-1: 93 records, 8,619 bytes, root recomputed and equal (section 7) |
| 08:36 | This page committed (ad25d2c3); the fork's day stream (f32641d1) |
| 08:4x | The coordinator relays the project lead's widening to proof of following; section 2a written (the refresh per window from the epoch's seed block, the pool and farm bound with the numbers: the floor is the state size, 6 KB today, 45 MB per member per hour is the WAN line at 10,000 members, a LAN farm is never bounded) |
| 08:42 | Section 2a committed (3cbe8c0f) |
| 08:56 | FIRST GREEN: igneum-pow class v5 (c6bbde78): blake2b, state leaves, the leaf XOR behind `Shape.state`, `V5_CLASS`, generator 5, the emitters' leaf buffer, the CLI's `--state`; 66 unit and 39 integration tests on igneum-build-1, the known-failed case (a stateless hasher) first |
| 09:10 | The fork builds (class-v5-node): `ProgramClass::V5`, `program_class_v5_activation_daa` (never, in the digest once set), the byte-5 tally and the one-step rule, the executor's per-epoch state captures and `igneum_getPowStateLeaves`, the engine's `DayStateProvider` and `DayStateUnavailable`, the miner's RPC provider with `--exec-rpc` and `--freeze-state`, the template's v5 fields (37 to 40); kaspa-pow 16 tests green; the harness `infra/fast-time/class-v5-signal.mjs` and the 4090 bench committed (e528cb05) |
| 09:1x | The remaining fork suites wait behind another lane's measurement hold on igneum-build-1 |
## 1. The claim, in one paragraph

View file

@ -191,6 +191,15 @@ Answer: Correct on both counts, and the second was the sharper one. The X9 (Bitm
Evidence: `docs/design/latency-ladder.md` (the rule, the hostile review, the measured verifier table, the X9 arithmetic); `igneum-pow/src/generator.rs` test `latency_ladder_known_failed_a_changed_n_was_a_hard_fork_and_rungs_are_class_v4`; the fork's `consensus/core/src/igneum.rs` test `latency_ladder_rule`, `consensus/pow/src/igneum.rs` test `latency_ladder_rungs_are_programs_of_their_own_over_one_day_cache`; `infra/fast-time/latency-ladder.mjs` (the step, no-step and known-failed cases).
### M35. "Proof of stored state" proves nothing a pool cannot ship, and "proof of following" is a 32 ms rebuild per hour
"Class v5 keys the dataset by the chain's state so that every hash proves the miner holds the chain. The state is 6 KB. A pool ships it with the template. The hourly refresh is a 32 ms rebuild that a farm's one node does once and broadcasts. Nothing on the chain can tell a card that derived the leaves from a card that received them, so the class proves nothing about who holds what, and it adds consensus-critical serialisation code for the privilege."
Status: Implemented behind a switch (7 October 2026, `docs/design/class-v5-stored-state.md`, branch `class-v5`, fork branch `class-v5-node` from the 0.3.18 node, behind `program_class_v5_activation_daa`, never until set): the dataset of every epoch is built from the canonical state stream after the epoch's seed block (`igneum/exec/src/day_stream.rs`: accounts, non-zero slots, code chunks, one serialisation that rebuilds to its root), hashed into leaves `D[i] = Blake2b-512('igneum-sd1/' || root || i || record)` and folded into every item (`leaf(t) = D[t mod n]`, so a hasher without the state, with another root, with a stream one record short or with the previous epoch's leaves is wrong on 64 of 64 items and 32 of 32 lanes: the known-failed case, `igneum-pow/src/state.rs`); the object byte 5 and the 95 percent seven-window tally beside class v4's (`consensus/core/src/igneum.rs` `program_class_for_epoch_signalled_v5`, one step per epoch); a node without the state refuses the header with a retryable error and serves no template (`kaspa-pow` `DayStateUnavailable`, the known-failed case `class_v5_refuses_without_state_and_refreshes_the_leaves_per_epoch`); the miner fetches the stream from its node's exec RPC (`igneum_getPowStateLeaves`); the fast-time gate `infra/fast-time/class-v5-signal.mjs` with a stateless node and a stale miner.
Answer: The first sentence is right and the page says it first: anything the lottery derives is derived from a seed and the state, so the only bytes a central node cannot compress away are the state's, 5,952 bytes at today's devnet state (93 records), and the chain cannot distinguish a card that derived the leaves from one that received them, exactly as it cannot distinguish a pool member from a solo miner today. The numbers are on the page: the leaves pass 45 MB per member per hour, the line where a WAN pool at 1 Gbit/s serving 10,000 members can no longer ship them inside the window, at about 700,000 state records; a LAN farm at 100 Gbit/s is never bounded below the 2 GiB sample cap. What the class does force, per machine and per hour: holding the current state or its leaves, a rebuild from it (32 ms on a 4090, measured on 6 October), and knowledge of the chain's reference block inside the ten-minute lead; a machine cut off from the chain for an hour stops producing valid blocks at the next refresh, where under a daily rule it kept mining until midnight. It also removes the recompute chip (the f = 0 row of chip-model-v3) as a category and moves nothing against the dataset-storing chip, which the page and the litepaper both say. The cost is measured and small (hash rate and watts unchanged on the 4090, build +1.4 ms resident, verifier +0.11 to 0.21 ms per unit); the risk is the serialisation, which is why the stream must rebuild to its root on the capturing node before it is served and why the gate crosses a day boundary with a non-trivial state before Devnet 2.
Evidence: `docs/design/class-v5-stored-state.md` (sections 2, 2a, 4, 5, 8); `igneum-pow/src/state.rs` tests; the fork's `igneum/exec/src/day_stream.rs` tests (`a_tampered_stream_is_refused`), `consensus/core/src/igneum.rs` (`program_class_signal_rule`, the v5 cases), `consensus/pow/src/igneum.rs`, `igneum/exec/src/service.rs` (`epoch_state_captures_follow_the_cut_and_unwind`); `infra/fast-time/class-v5-signal.mjs`.
## 2. Finality and attacks
### F1. Finality is attackable for the first month

View file

@ -899,3 +899,101 @@ fn hot_packs_emitted_sources_and_load_forms() {
assert!(ph.contains("igneum_launch_hot_fill("));
}
}
// ---------------------------------------------------------------------------------------------------------------
// Class v5 (docs/design/class-v5-stored-state.md, 7 October 2026): the pinned pack under proto-cuda/packs-ca3-v5/
// ---------------------------------------------------------------------------------------------------------------
fn v5_packs_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda/packs-ca3-v5")
}
fn v5_read(pack: &str, file: &str) -> String {
std::fs::read_to_string(v5_packs_dir().join(pack).join(file)).unwrap_or_else(|e| panic!("{pack}/{file}: {e}"))
}
fn v5_json(pack: &str, file: &str) -> Value {
serde_json::from_str(&v5_read(pack, file)).unwrap()
}
/// The epoch of a pinned class v4 or v5 pack of the string seed and day: the program through the seam, the dataset at
/// the day-0 size, and for a v5 pack the leaves of its `state.igsd1` (the devnet's state stream of 7 October 2026,
/// node 1's exec snapshot at chain block 159,357: 93 records, root 0x1c583d35...).
fn v5_epoch(pack: &str) -> Epoch {
let j = v5_json(pack, "program.json");
let seed = j["seed"].as_str().unwrap();
let class = ProgramClass::parse(j["program_class"].as_str().unwrap()).unwrap();
let program = generate_from_seed_bytes_program_class(seed, seed.as_bytes(), class, None);
let day = j["dataset"]["day"].as_str().unwrap();
let log2 = j["dataset"]["log2_words"].as_u64().unwrap() as u32;
let shape = Shape::for_class(&program.class);
let mut dataset = DatasetSource::new_shape(day, DatasetMode::MemoryHard, log2, shape);
if class == ProgramClass::V5 {
let stream = igneum_pow::StateStream::read_file(&v5_packs_dir().join(pack).join("state.igsd1")).unwrap();
dataset = dataset.with_leaves(std::sync::Arc::new(igneum_pow::StateLeaves::from_stream(&stream, log2)));
}
Epoch { program, dataset }
}
/// The class v5 pack is the class v4 program of the same seed over the state leaves: generator 5 and
/// `program_id(5, seed, attempt)`, the base program, the shadow block and the dataset's cache equal to the v4 pack's,
/// every vector and dataset word different, every emitted file byte for byte what the crate exports (leaves.bin
/// included, its FNV in program.h), and the hash kernel text of kernel.cu equal to the v4 pack's but for the build
/// kernel and the class lines. The known-failed case first: the v4 control pack's vectors under the v5 epoch agree on
/// no lane.
#[test]
fn v5_pack_is_the_v4_program_over_the_state_leaves() {
let e5 = v5_epoch("v5-genesis");
let e4 = v5_epoch("v4-genesis");
let v4 = v5_json("v4-genesis", "vectors.json");
// the known-failed case: the v4 pack's hashes are not the v5 epoch's on any lane
let out4: Vec<u64> = v4["warps"][0]["out"].as_array().unwrap().iter().map(hex64).collect();
let got5 = e5.hash_warp(0);
assert_eq!(out4.iter().zip(got5.iter()).filter(|(a, b)| a == b).count(), 0, "0 of 32 lanes of the v4 pack agree with the v5 epoch");
assert_eq!(e4.hash_warp(0).to_vec(), out4, "the v4 control pack is the v4 epoch");
// the program: v4's draw, generator 5, the state in the class and the id
assert_eq!(e5.program.generator, igneum_pow::GENERATOR_VERSION_V5);
assert_eq!(e5.program.class, igneum_pow::V5_CLASS);
assert_eq!(e5.program.class.name(), "mx8+sh256x27+state");
assert_eq!(e5.program.instrs, e4.program.instrs);
assert_eq!(e5.program.shadow, e4.program.shadow);
assert_eq!((e5.program.seed, e5.program.attempt), (e4.program.seed, e4.program.attempt));
assert_eq!(e5.program.program_id(), igneum_pow::generator::program_id(igneum_pow::GENERATOR_VERSION_V5, &e5.program.seed, e5.program.attempt));
assert_ne!(e5.program.program_id(), e4.program.program_id());
// the dataset: the same cache, other items
let m5 = e5.dataset.memhard().unwrap();
let m4 = e4.dataset.memhard().unwrap();
assert_eq!(m5.cache.fnv1a64(), m4.cache.fnv1a64(), "one day cache");
assert!(m5.shape().state && !m4.shape().state);
let leaves = e5.dataset.leaves().unwrap();
assert_eq!((leaves.n(), leaves.records_total, leaves.sampled), (93, 93, false));
assert_ne!(e5.dataset_word(0), e4.dataset_word(0));
// every file as the crate exports it, leaves.bin included
for pack in ["v4-genesis", "v5-genesis"] {
let e = if pack == "v5-genesis" { &e5 } else { &e4 };
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 {
assert_eq!(v5_read(pack, name), *text, "{pack}/{name} differs from the export");
}
for (name, bytes) in &out.binaries {
assert_eq!(std::fs::read(v5_packs_dir().join(pack).join(name)).unwrap(), *bytes, "{pack}/{name}");
}
assert_eq!(out.binaries.len(), (pack == "v5-genesis") as usize);
}
let h5 = v5_read("v5-genesis", "program.h");
assert!(h5.contains("#define IGNEUM_PROGRAM_CLASS \"v5\"") && h5.contains("#define IGNEUM_STATE_LEAVES 93") && h5.contains(&format!("#define IGNEUM_STATE_LEAVES_FNV64 {}", igneum_pow::emit::hex64(leaves.fnv1a64()))));
assert!(h5.contains("igneum_launch_build(uint32_t* ds, const uint32_t* cache, const uint32_t* leaves, uint32_t nLeaves, uint32_t nItems)"));
// the hash kernel text is class v4's byte for byte; the build kernel and the class lines are what differ
let hash_text = |s: &str| {
let a = s.find("__global__ void igneum_hash(").unwrap();
let b = s.find("// Host-side launch wrappers").unwrap();
s[a..b].to_string()
};
let k5 = v5_read("v5-genesis", "kernel.cu");
let k4 = v5_read("v4-genesis", "kernel.cu");
assert_eq!(hash_text(&k5), hash_text(&k4), "the hash kernel is class v4's");
assert!(k5.contains("mh_item(cache, mh_leaf(leaves, nLeaves, t), t, s)") && !k4.contains("mh_leaf"));
let mh5 = v5_read("v5-genesis", "memhard.h");
assert!(mh5.contains("s[i] ^= leaf[i]"), "the leaf XOR before the first mixer");
}

View file

@ -345,6 +345,7 @@ body.all .pager{display:none}
</table></div>
<p>Three ideas carry the chip resistance. <strong>The hash rewrites itself.</strong> A new program every hour, drawn from the chain. Its memory pattern changes with it. The rules change on a schedule fixed at launch. No release, no vote. These are automatic schedule changes: they defeat a chip wired for one datapath and they need no human fork. Against a chip that stores the dataset every drawn parameter is firmware, and what meets that chip is the latency-shadow work (class v4) and the price per joule (the Horizon lane analysis, 6 October 2026, section 5.4; ledger M32). <strong>It waits on memory, not maths.</strong> Every hash is a chain of random reads into a table too big for a chip to carry. The wait is the same physics for everyone. <strong>Miners hold the switch.</strong> Spare defences are written into the rules, switched off. A miner signal turns one on, at the class-change threshold: miners signal three things at three thresholds, 60 percent of blue blocks over two weeks for a parameter genesis leaves open, 90 percent for an upgrade (new code), and 95 percent with a floor height for a class change. No fork.</p>
<p><strong>The work that waits can grow.</strong> Class v4 adds a block of latency-shadow arithmetic to every hash, about 100,000 integer operations that run while the memory reads are in flight, so a chip that stores the whole dataset still has to pay for a core. That size sits on a ladder fixed at genesis, six rungs from about 100,000 to about 1,000,000 operations, and it moves one rung at a time only when 90 percent of blue blocks in each of seven consecutive days ask for it; it can never move two rungs inside a week and never past a rung the reference verifier cannot check under 10 ms with its sibling thread busy (measured on the build server, 6 October 2026: the first three rungs pass at 8.8, 8.9 and 9.2 ms, the fourth misses by 0.08 ms on a loaded box and stays out until a quiet re-measurement, the two doublings are out at 12.4 and 15.0 ms). What it buys, on the measured cards: against a dataset-storing chip whose core costs what an RTX 5090's does per operation, the chip's per-joule edge falls from 2.1x at the first rung to 1.3x at the third; against a core as good as the shipping RandomX chip's (Bitmain's Antminer X9, about 3x per joule over a desktop CPU after seven years, approximate), from 3.9x to 2.8x. What it costs, per rung, is measured too: the Apple tier gives up 3 points of rate at the first step and 6 more at the second, the RTX 5090 nothing until the second; so the miners who pay for a step are the ones who take it (<a href="/ledger#M34">ledger M34</a>).</p>
<p><strong>The dataset is the chain.</strong> Class v5 builds each hour's dataset from the chain's own execution state at that hour's reference block: every account, every storage slot and every byte of contract code, hashed under the state root and folded into every item. A card that does not hold the state cannot build the dataset, and a card that builds it from stale state is wrong on every hash from the next refresh on, so a miner must keep following the chain to keep mining. The hash itself does not change, and neither does its speed: measured on an RTX 4090 on 6 October 2026, 63.08 against 63.09 million hashes a second at 207 W, the hourly rebuild 32 ms, a node's check 0.1 to 0.2 ms longer per block. What it buys is a floor under what mining means: a chip that recomputes items instead of storing them must now hold the state too, and a pool miner who today needs nothing but the date needs the state every hour. What it does not buy is stated as plainly: a pool or a farm with one node can ship the state to its cards each hour, 6 KB today and at most 2 GB on a used chain, so the claim is that every mining operation holds and follows the chain, not every card; and against a chip that stores the whole dataset it changes nothing, which is why it sits beside the latency-shadow work, not in its place. It ships switched off, behind the 95 percent class signal with a floor height (<a href="/ledger#M35">ledger M35</a>).</p>
<p>No hash has stayed free of chips forever. Igneum does not claim to. It states the gain its own model finds, the response takes a week, and both are measured. The model is public: <a href="/bench#counter-asic-2-0-the-numbers">the numbers</a>; the claim is tested by paid independent cryptanalysis and the public benchmark. Monero ran on RandomX from 2019 (approximate) with no chip publicly shipped until Bitmain’s Antminer X9 in July 2026 (1 MH/s at 2,472 W, about USD 5,600; monero-project/monero issue 10270). About seven years of hold and then a chip: that is the record Igneum’s hourly program and its chip model are built against.</p>
<p>One thing takes a person, here and on every chain that exists: writing new code. A chain cannot safely write its own generator, and it cannot safely tell a chip from a wave of honest new cards by hashrate alone. If the design above ever failed, anyone could publish a new generator and miners would switch it on by signalling, as Monero's community can fork. Igneum is built to make that day unlikely, and does not depend on avoiding it.</p>
</section>