24 KiB
The latency ladder: N as a verifier-bounded genesis ladder stepped by miner signal
6 October 2026, night UK. Horizon finding 4 (docs/analysis/horizon/algorithm.md 5.3a and proposal 5; frontier.md 2.3). Branch ladder (repo) and ladder-node (fork, from ca3-v4-0316 8dbb7a23). Status: implemented behind latency_ladder_activation_daa (never until set; 0 on the testnet when the project lead says), ships in the 0.3.17 feature tree (0.3.16 went out on 6 October 2026 as a corrective cut of 0.3.15, node f1ea7a38, and carries none of this); not in docs/spec until adopted.
1. The finding in one line
The chip that matters stores the dataset; the reserve and the era draw buy nothing against it; the one lever is the latency-shadow size N, and an unconditional doubling per era retires the M5 Max at era 1 (-10.5 percent at 200,000 ops). So N moves inside a ladder fixed at genesis, one rung at a time, only when 90 percent of mining weight asks for it, never past the rung the reference verifier can still check under 10 ms.
2. Where N lives
| Item | Place | Value |
|---|---|---|
| The unit | igneum_pow::ShadowClass { instrs: 256, reps }: class v4 is mx8 plus a 256-instruction shadow block run reps times per iteration; N in counted ops = 930 + 2,048 x reps x 1.83 (the convention of latency-shadow-2026-10-06.md s1) |
reps is what the generator consumes; the ops label is derived (igneum::latency_ladder_counted_ops) |
| The ladder | Params::latency_ladder (consensus/core/src/config/params.rs), a genesis list of six rungs { reps, admissible }, in the override file as latency_ladder: [{"reps": 27, "admissible": true}, ...], in the digest once the activation is set |
reps 27, 35, 53, 88, 173, 267 = about 102,100, 132,100, 199,600, 330,700, 649,400, 1,001,600 counted ops: the measured rungs of 5.3a (35 passes is the integer rung nearest the 130,000 interpolation), then the two doublings; admissible tonight: rungs 0, 1, 2 (section 5) |
| The switch | Params::latency_ladder_activation_daa (u64::MAX never; in the digest once set, the 0.3.15 rule, so a binary that carries the field peers with one that does not until a file sets it) |
never on every network; 0 on the testnet when the project lead says |
| The window | Params::latency_ladder_window_daa (one window of the seven; in the digest once the activation is set) |
86,400 DAA (one day); 120 on the fast-time profile, the class window's rounding |
| The step | consensus state derived from signals, never stored: processes::latency_ladder::step_of_epoch walks the epochs' seed blocks down the selected chain exactly as class_signal::program_class_of_epoch does, memoised per seed block |
rung 0 before the activation and at it |
| The era draw | draws 8 (epoch_len) and 9 (the ladder) of the era stream are consumed and not used (igneum_pow::era_draw), so a later use of either slot changes no other parameter; the first seven draws and every pinned pack are unchanged |
the value is set by signal, not by the draw, as epoch_len is (spec 01 1.13.1) |
| The program | EpochSeeds.shadow_reps (0 = the class's own) → Epoch::chain_program_shadow → generate_from_seed_bytes_program_class_shadow: class v4 at rung r is LoadClass::era(v4_class_at(reps_r), era, &V3_ALLOWED), generator 4; rung 0 is V4_CLASS byte for byte |
the base program, the 16 loads, the day cache and the era draw are class v3's; only the block count moves |
3. The step rule (igneum::latency_ladder_step_signalled, pure; the tally shared with the class signal)
- The carrier: two bits of the header
versionhigh byte. Bit 15 = up, bit 14 = down, both = no signal; the object version (the class signal) keeps bits 8 to 13 (class_signal_ofmasks with 0x3f; every value written so far is 4, so nothing changes). Stamped by the node fromParams::latency_ladder_signal(IGNEUM_LADDER_SIGNAL=up|down|noneon devnet and simnet; the app's toggle later) only while the ladder is active; the header version rule accepts the high byte while the class signal or the ladder is active. - The weight and the windows: blue blocks, the finality rule's convention,
CLASS_SIGNAL_WINDOWS = 7consecutive windows oflatency_ladder_window_daaending at the epoch's seed blockS_e, one walk bucketed per window (class_signal::tally_windowwith the ladder predicate). The number the decision rests on is the weakest of the seven. - The decision for epoch e, from the step of epoch e - 1: up one rung when every one of the seven windows is full, each has at least
LADDER_SIGNAL_THRESHOLD_BPS = 9,000of its blue blocks signalling up, the rung above exists and is admissible, and the oldest window begins at or after the DAA score where the current step took effect; down one rung symmetrically (never below rung 0). Otherwise the step stands. 90 percent, not 95: the ladder is a parameter the genesis rules leave to miners within fixed bounds (theepoch_lenprecedent, spec 01 1.13.1), not a class change; 60 percent (a parameter the rules leave open) is too low for a move that retires cards. - Where it lands: at the epoch boundary, decided at the seed block, so the miner learns the next epoch's rung one lead (600 DAA) before it, with
next_latency_ladder_repsin every template; a class never changes inside an epoch, nor does a rung. - The cool-down (the two-step guard): because the seven windows must lie wholly after the last step, the earliest next decision is 7 x W (seven days) after a step, and every counted signal was cast under the rung it moves from. A miner majority cannot jump two rungs in one decision and cannot take two decisions inside seven days.
4. The verifier bound
A rung is admissible only if the cold verify of one 32-lane warp on the reference core stays under 10 ms with the SMT sibling loaded. Measured as lane 2 measured class v4: igneum-pow bench --seed igneum-genesis --day 2026-10-03 --class mx8+sh256x<reps> --warps 50 on igneum-build-1 (EPYC 9454P, core 40 at 3.8 GHz under schedutil, nice -n 19 taskset -c 40), the figure is warp base 0: single cold run with the same bench on core 88 (the sibling, thread_siblings_list 40,88) at the same time; the average of 50 warps and the quiet-core cold run are recorded beside it. The measurement runs under the box's measure hold (remote-run.sh BR_MEASURE=1: every build slot waits, the capacity layer yields), tools/ladder/verify-bench-remote.sh. The table is section 5; a rung over 10 ms is admissible: false in the genesis list itself, and the step rule never enters it. Changing a flag after genesis is a code change under the 90 percent upgrade path (spec 05 5.7); the owed O-1.14 laptop run can only tighten the list before genesis.
5. The measured table (igneum-build-1, 6 to 7 October 2026)
Run 2 (22:10Z, docs/design/latency-ladder-bench/20261006T221034Z, tools/ladder/verify-bench-remote.sh under the measure hold; core 40 read 1,500,000 kHz at the start under schedutil; box load average 25.4 before and 23.6 after from other agents' processes the hold does not exclude; igneum-pow sha256 3f7623f2...; every rung's lane-0 hash equal across the three runs of a rung):
| Rung | reps | Shadow instrs per hash | Counted ops (approx) | Cold, core alone (ms) | Avg of 50, alone (ms) | Cold, sibling loaded (ms) | Avg of 50, sibling loaded (ms) | Admissible |
|---|---|---|---|---|---|---|---|---|
| 0 | 27 | 55,296 | 102,100 | 5.14 | 4.95 | 8.77 | 8.67 | yes (class v4 today) |
| 1 | 35 | 71,680 | 132,100 | 5.57 | 5.52 | 8.87 | 8.51 | yes |
| 2 | 53 | 108,544 | 199,600 | 5.57 | 5.35 | 9.23 | 8.91 | yes |
| 3 | 88 | 180,224 | 330,700 | 6.07 | 5.89 | 10.08 | 9.84 | NO (by 0.08 ms under a load of 25; a quiet re-run before genesis may admit it) |
| 4 | 173 | 354,304 | 649,400 | 7.38 | 7.23 | 12.38 | 12.09 | NO |
| 5 | 267 | 546,816 | 1,001,600 | 8.88 | 8.50 | 14.96 | 14.58 | NO |
Reading: the loaded slope is about 12.6 us per 1,000 extra shadow instructions (8.77 to 14.96 over 491,520), lane 2's 12.1; rung 0 reads 8.77 against lane 2's 8.23 (its load average was 2.3), so the box's other work costs about 0.5 ms and rung 3 would read about 9.6 ms on a quiet box. The rule is the measurement, not the estimate: the genesis list carries rung 3 as inadmissible tonight, and a re-measurement on a quiet box (or the O-1.14 laptop) before the testnet genesis file is written can admit it. Per tier what the flags mean: the ladder as it ships can take N from 102,100 to 199,600 ops (two steps, each at least seven windows apart); at the top rung the Apple tier has given up about 10 percent of rate, the 5090 2.7 percent at its cap, the 4070 pays 21 W more, and a node on a 2019-class core still verifies a warp inside 10 ms with its sibling busy.
Lane 2's slopes predicted 8.23 + 12.1 us per 1,000 extra shadow instructions on the loaded core: 8.4, 8.9, 9.7, 11.9, 14.2 ms for rungs 1 to 5, so rungs 4 and 5 were expected out before the run. The genesis list in params.rs carries the flags this table gives. First run (22:06Z, docs/design/latency-ladder-bench/20261006T220642Z): the "loaded" column was NOT loaded (the known-failed case of the script: a 50-warp sibling finished during the measured run's own cache fill, which the fill time showed, 540 against 366 ms, while the warps then ran alone: 5.08, 5.20, 5.50, 6.07, 7.39, 8.83 ms); the script now keeps the sibling busy for the whole run and the table above is the second run's.
Also recorded in the first run: the quiet-core cold figures 5.09, 5.20, 5.51, 6.04, 7.45, 8.90 ms for rungs 0 to 5 (lane 2's class v4 cold figure was 5.06), so on the quiet reference core every rung including 1.0 M ops passes 10 ms; the loaded column decides.
5a. The chip anchor: what a determined vendor did to RandomX in seven years (coordinator's data point, 6 October 2026, night)
Bitmain's Antminer X9 is a shipping RandomX ASIC: about 1 MH/s at 2,472 W (2.47 J per KH), custom RISC-V cores, about USD 5,600, shipping July 2026 (github.com/monero-project/monero/issues/10270; every figure approximate, read from the issue by the coordinator). A Zen 4 desktop CPU at roughly 20 KH/s and 150 W is 7.5 J per KH (approximate), so the chip's per-joule edge over the honest device RandomX was built for is about 3x, seven years after launch. Two consequences for this project's own text and model:
| Item | Before | After |
|---|---|---|
| The precedent sentence | latency-shadow-2026-10-06.md s5 and site/litepaper.html: "no RandomX chip has beaten a CPU per joule", "no chip publicly shipped" |
false since July 2026; corrected in the litepaper tonight (section 10 below) and owed in latency-shadow-2026-10-06.md and chip-model-v3.md |
| The k column | k = 0.3 was "the attacker's claim" (a wide-SIMD fixed-datapath array at N5); k = 1 was "ours" and the headline (2.1x over the 5090 at class v4) | a shipped product reached about 1/3 of its honest device's energy on a latency-bound random program, so k about 0.33 is a measured class, not a claim; the honest headline is the range, 2.1x (k = 1) to 3.9x (k = 0.33) over the 5090 at rung 0, with the X9 bracket named. Approximate in both directions: the 5090's ALU is already a wide-SIMD part at 11 pJ per op, so a chip's gain over it on random ALU work should be under its gain over a CPU core, and the memory side (the f = 1 chip) is priced separately and is not moved by k |
| The 5.7x | the f = 1 GDDR7 chip's per-joule edge at class v3 (memory only) | unchanged: no core work in it, k does not enter |
What N buys against a vendor at the X9's scale (energy per hash = memory + N x 11 pJ x k, latency-shadow-2026-10-06.md s5; the 5090 and M5 Max rows measured, algorithm.md 5.3a; k = 0.33; approximate):
| Rung | N (approx) | f = 1 GDDR7 chip, uJ | Edge over the 5090 at its 431 W cap | Edge over the M5 Max |
|---|---|---|---|---|
| 0 | 102,100 | 0.84 | 3.9x | 1.7x |
| 1 | 132,100 | 0.95 | 3.4x | 1.5x |
| 2 | 199,600 | 1.19 | 2.8x | 1.3x |
| 3 | 330,700 | 1.67 | 3.0x (the 5090 itself is compute-bound at this rung, 86 MH/s) | 1.1x |
So against a vendor at the X9's scale the ladder narrows the chip's edge over the 5090 from about 3.9x to about 2.8x by rung 2 (the top admissible rung tonight) and does not close it; against the Apple tier it takes the edge to about 1.3x at rung 2 and would take it to about 1.1x at rung 3, where the Mac has given up 21 percent of its rate and which the verifier measurement keeps out until a quiet re-run. The lever that moves every row by the same factor is the honest card's own watts (algorithm.md proposal 7), and the public text says so. The ladder is the chain's only automatic answer; it is not the whole answer.
6. What moves the digest and the wire
| Change | Digest | Wire |
|---|---|---|
| The binary carrying the fields, nothing set | no (the 0.3.15 rule: the activation and the window enter only once set; the ladder list enters only once the activation is set) | header version unchanged; PowEpochInfo gains fields 25 to 36 with defaults, so an old miner reads reps 0 (the class's own) |
The file setting latency_ladder_activation_daa (and the window, and the list) |
yes, once, at publish | the high byte may carry bits 14 and 15; a node without the ladder refuses the version when class signalling is inactive, which is the point of the rollout order (binary first, file second) |
| A step | no | the program of the epoch changes: program_id_class with the shadow/ bytes (rung 0 keeps program_id(4, seed, attempt)), the pack's IGNEUM_SHADOW_REPS and seeds.txt's shadow_reps line (absent at rung 0, so every pack and job line written today is unchanged; the job line gains no token, because the three hosts strip only class= and era= from its tail) |
7. Hostile review
| Question | Answer |
|---|---|
| Can a chip-holder vote the ladder down? | Down needs 90 percent of blue blocks in each of seven windows. A chip at 10 percent of hash cannot; at 90 percent the chain has a bigger problem than N, and the honest 10 percent still blocks the move (a 10 percent minority stops any move either way). |
| Can a chip-holder stall it? | Yes, with over 10 percent of weight for the whole week: the status quo is kept. The status quo is an admissible rung the cards already run, so a stall costs the chain nothing it does not have today; the ladder is a lever miners pull, not a schedule a chip can be late for. The one defence it does not give is a forced move against a chip that holds 10 percent: that is the honest-card watts and the price per joule (algorithm.md 5.1). |
| Can a miner majority jump two steps? | No: one rung per decision, and the next decision's seven windows must begin after the step took effect, so two moves are at least 7 x W apart (latency_ladder_step_signalled, tested). |
| Can a week be bought? | 90 percent of blue blocks for seven consecutive days is 90 percent of hash for a week, in public, with the pending count on every console (latency_ladder_up_weakest_bps); the class signal's argument (one day can be bought, seven cannot unnoticed). |
| What happens to a miner mid-step? | The rung is decided at the seed block, 600 DAA before the boundary; the template carries next_latency_ladder_reps and the miner prepares the next pack with it (the 2.0 era-boundary shape: a boundary that decides otherwise costs one refused pair and one prepare). A miner on a binary without the ladder keeps mining rung 0 after the step and loses every block until it updates; it can be at most 10 percent of hash by construction, and it had seven days of a rising count to see it. |
| An inadmissible rung by a 90 percent majority? | Impossible: the rule reads the flag; the flag is a genesis constant in the digest. |
| A proof-synced node | holds no headers below its pruning point, so an epoch whose windows reach below it cannot be tallied; it takes rung 0 (the floor) and logs that a ladder witness is owed, the class signal's shape (counter-asic-3-node.md 6.3). The witness in the proof format is the next node item, shared with the class witness. |
| The program id | rung 0 keeps today's id; a rung above goes through program_id_class, which carries the shadow size, so two rungs of one seed never share an id and a pack of another rung is refused by the id check as a pack of another class is. |
8. Consequences per tier (measured rungs from algorithm.md 5.3a; the step is the miners' to take)
| Step | M5 Max | RTX 5090 at 431 W | RTX 4070 at 160 W | RX 9070 XT | Pool user | Verifier (half-core) |
|---|---|---|---|---|---|---|
| 0 → 1 (102,100 → 132,100) | -3.3 points of rate, 0 W more | 0 | 0 | 0 | 0 | +0.2 ms |
| 1 → 2 (→ 199,600) | -6 more points | -2.7 percent | +21 W | 0 | 0 | +0.5 ms |
| 2 → 3 (→ 330,700) | -21 percent | -35 percent (compute-bound at the cap) | -12 percent | +3.6 percent | 0 | +0.9 ms |
| 3 → 4, 4 → 5 | unmeasured on cards; the verifier decides admissibility (section 5) |
A chip's shadow core grows with N at k x 11 pJ per op; at rung 1 the f = 1 GDDR7 chip's edge over the 5090 at k = 1 falls 2.1x to 1.7x, at rung 2 to 1.3x. Which is why the step is the miners' who pay for it: rung 3 is a step the signal would refuse until cards change.
9. What is implemented, tested, owed
Public text: section 10.
Test and build lines (igneum-build-1 through tools/build-remote.sh, IGNEUM_AGENT=ladder, 6 October 2026, 22:00 to 22:20Z): igneum-pow test --release: 61 passed (the first run failed on the rung-1 label 130,000 against the arithmetic's 132,105, corrected to the arithmetic); the fork at 1591ee1d: kaspa-consensus-core 114 passed, 2 ignored (latency_ladder_rule, override_params_carry_the_latency_ladder_and_the_digest_moves_only_when_set, the 60x file test after the file gained the two 0.3.16-lane fields and exec_restart_state_root it lacked); kaspa-pow with the feature 16 passed (latency_ladder_rungs_are_programs_of_their_own_over_one_day_cache: one cache build for three rungs, rung 1 another pow, 27 passes equal to rung 0); kaspa-consensus lib 131 passed (the template carries bit 15 only while the ladder is active); igneum-miner, kaspa-rpc-core, kaspa-grpc-core green; igneumd and igneum-miner built, the commit string carried. Harness results: section 11.
Implemented: the ladder, activation, window and signal in Params, OverrideParams, the digest and the 60x file; the pure rule and the bits in igneum.rs; processes::latency_ladder (the walk, the memo, the step); EpochSeeds.shadow_reps through the engine, the header path, the miner and the pack; PowEpochInfo and the gRPC fields; the daemon lines; igneum-pow: v4_class_at, the shadow program path, the id rule, the consumed draws, --shadow-reps on the CLI. Tests, the known-failed case first: a changed N today is a hard fork (the same seeds at reps 35 hash another program with the same class and, before the ladder, the same id); the step moves only by signal; 8,999 bps does not move it; a two-step jump is impossible; down never below 0; an inadmissible rung is never entered; rung 0 is V4_CLASS byte for byte. Fast-time harness infra/fast-time/latency-ladder.mjs (three cases and the failed case, the class harness's shape). Owed: the ladder witness in the pruning proof; the app's signal toggle and its consequences line; a rung check in the three hosts (today a host compiles whatever IGNEUM_SHADOW_REPS the pack carries and the Rust pack check binds the pack to its seeds, so a stale pack of another rung fails the miner's own check, not the host's); the O-1.14 laptop run.
10. Public text (the litepaper's hash-class paragraph, site/litepaper.html, Mining section) and the ledger row
The paragraph as placed in the litepaper: "The work that waits can grow. 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: every rung's cold verify stays under that gate on the quiet core, 5.1 to 8.9 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." The sentence "Monero has run on RandomX since 2019 with no chip publicly shipped" is replaced by the X9 fact.
The ledger row is M34 in docs/fud-ledger.md.
11. The fast-time gate (igneum-build-1, 6 October 2026, 22:15 to 22:58Z; summaries and logs copied to docs/design/latency-ladder-harness/; infra/fast-time/latency-ladder.mjs, the ladder fork's Linux binaries built on the box, three nodes and three CPU miners on override-60x.json with class v4 from genesis, class signalling off, the ladder active from DAA 0, windows of 60 DAA)
| Case | Run | SUMMARY | Summary file |
|---|---|---|---|
The known-failed case first (--signal up,up,none --expect step) |
22:15 to 22:26Z | FAIL as it must: no step over epochs 0 to 10, the weakest-of-seven up share 6,166 to 6,333 bps at the sink from epoch 7 (two CPU miners of three), on the chain 406 blocks up and 198 none (6,722 bps up), no step line on any node, 0 rejected, one sink at 603/603/603; FAILED CHECK on eight step checks plus chain_carries_the_bits, which was the harness's own fault (genesis carries header version 0 and the low-byte check read it; fixed in the file and recorded there), exit 1 |
/srv/builds/_log/ladder-harness/failed-case.json |
All three signal up (--signal up,up,up --expect step) |
22:26 to 22:37Z | the chain did what the rule says: Latency ladder step by miner signal: epoch 8 moves to rung 1 (35 shadow passes, from rung 0): up in each of 7 consecutive windows of 60 DAA ending at seed block beb8b40f..., threshold 9000 bps, weakest up 10000 bps, ... 420 of 420 blue blocks up on 3 of 3 nodes with the same epoch; the template at rung 1 from epoch 8 (DAA 480, the first epoch whose seed block has seven full windows below it) at 500 s wall; epochs 9 and 10 stayed at rung 1 (no second step inside seven windows); 481 / 132 blocks across the boundary; 0 rejected; one sink at 612/612/612; 612 blocks up and genesis none (9,984 bps up), every object byte 0; the three miners' rung-1 ids equal each other and differ from the same seed's rung-0 id on epochs 8, 9 and 10 (e8 d4cfc28a1c525077 against ea49bc6e870eb34c). The harness reported FAIL on two checks of its own: the genesis low-byte fault above and rung1_ids_equal_the_cli_rung1_id, because igneum-pow show built its program without --shadow-reps (fixed at 6b30e85; the miners' ids were the chain's). Re-run as step2 below with both fixes |
step.json |
Two of three signal up (--signal up,up,none --expect no-step --epochs 10) |
22:37 to 22:47Z | PASS: no step over epochs 0 to 10; the weakest-of-seven up share at the sink 5,833 bps from epoch 7 (the third miner's blocks carry no bits); on the chain 402 blocks up and 200 none (6,678 bps up); no step line on any node; 0 rejected; one sink bfc435d62d7c659c at 601/601/601; every rung-0 id equal to the CLI's |
no-step.json |
All three signal up, the fixed harness and CLI (--signal up,up,up --expect step, step2) |
22:47 to 22:58Z | PASS on all 18 checks: the step line on 3 of 3 nodes at epoch 8 (weakest up 10,000 bps, 420 of 420 blue blocks up); the template at rung 1 (35 passes) from epoch 8 at 505 s wall; epochs 9 and 10 at rung 1, one step line per node (no second step inside seven windows); 481 / 137 blocks across the boundary; 0 rejected; one sink f1cbdb99f48e0a19 at 617/617/617; 617 blocks up and genesis none (9,984 bps), every object byte 0, every mined block's low byte 2; the three miners' rung-1 ids on epochs 8, 9 and 10 equal the CLI's --shadow-reps 35 id and differ from the same seed's rung-0 id |
step2.json |