39 KiB
Difficulty controller for Igneum: data, simulation, implementation, test network
3 October 2026, consensus-engineer. Everything measured here is reproducible from sim/difficulty/
(record, simulator, raw outputs) and the difficulty branch of vendor/igneum-node.
1. The question
Igneum's effective hash rate steps every hour when the mining program changes (35 to 48 Mhash/s
across seeds on the M5 Max, 228 vs 185 Mhash/s for 104 vs 128 loads on the RTX 5090,
docs/bench-log.md), and GPU miners hop in and out fast. Kaspa's sampled DAA (KIP-4) averages a
44-minute window and holds genesis bits for 600 blocks. The devnet of 3 October showed what that
costs. This document measures five controllers on synthetic steps and on the devnet record, tunes
the Igneum candidate on the synthetic set only, then validates it on the record and on a live
3-node test network.
2. Data: the overnight devnet record
sim/difficulty/devnet-2026-10-03.csv: 3,682 headers pulled through the observer node's wRPC JSON
endpoint (getBlocks from genesis, read only), columns hash, DAA score, blue score, timestamp,
bits, difficulty (2^255 / target, the node's own convention; expected hashes per block is twice
that), log2 target, epoch, chain flag, parent count, selected parent. Genesis bits 0x1d100000
(difficulty 134,217,727, 2^28 expected hashes per block), sized for the RTX 5090 plus the M5 Max.
Timeline (block 1 at 19:07:49 UTC = 20:07 BST):
| Minutes from block 1 | Blocks | Blocks/s | Bits at end | Difficulty at end | DAA score at end | What was mining |
|---|---|---|---|---|---|---|
| 0 to 5 | 36 | 0.12 | 0x1d100000 | 134,217,727 | 35 | Mac Metal worker, 30 MH/s |
| 5 to 10 | 43 | 0.14 | 0x1d100000 | 134,217,727 | 78 | same |
| 10 to 15 | 24 | 0.08 | 0x1d100000 | 134,217,727 | 102 | same |
| 15 to 20 | 34 | 0.11 | 0x1d100000 | 134,217,727 | 136 | same, stopped at 19:28 UTC |
| 20 to 25 | 3 | 0.01 | 0x1d100000 | 134,217,727 | 139 | about 1.5 MH/s (three isolated blocks) |
| 25 to 30 | 1 | 0.00 | 0x1d100000 | 134,217,727 | 140 | same |
| 30 to 35 | 9 | 0.03 | 0x1d100000 | 134,217,727 | 149 | PC joins at 19:42 UTC |
| 35 to 40 | 172 | 0.57 | 0x1d100000 | 134,217,727 | 321 | PC, 116 MH/s estimated from the blocks (coordinator: 154 MH/s plus 4 MH/s AMD) |
| 40 to 45 | 187 | 0.62 | 0x1d100000 | 134,217,727 | 507 | same |
| 45 to 50 | 421 | 1.40 | 0x1e00b6a3 | 11,758,225 | 925 | first retarget at DAA 600 (19:56:12 UTC): bits 0x1d4caca1, then easing every block |
| 50 to 55 | 1,633 | 5.44 | 0x1e00d950 | 9,882,007 | 2,559 | difficulty bottoms at 8,552,118 (15.7x below genesis); peak 355 blocks in one minute |
| 55 to 60 | 639 | 2.13 | 0x1d36b2ef | 39,260,045 | 3,201 | the window fills with fast blocks, difficulty climbs past the correct level |
| 60 to 65 | 400 | 1.33 | 0x1d3a4442 | 36,856,148 | 3,599 | still above the correct level (about 58 million for 116 MH/s at 1 block/s) |
| 65 to 70 | 79 | 0.26 | 0x1d44da0d | 31,189,959 | 3,680 | epoch boundary at DAA 3,600; the PC's launcher rebuilds its kernel (miner exit 42) and the chain idles |
What Kaspa's rule did, as the coordinator saw it live and the record confirms: genesis difficulty held through block 600 while the real rate was 0.6 blocks/s; the first retarget saw a window whose 600 blocks spanned 48 minutes (the Metal period, the idle gap, the PC period) and eased 4.8x at once, then kept easing to 15.7x as more of the slow history entered the average; the chain ran at 5.4 blocks/s for five minutes (1,633 blocks) and 355 blocks in its peak minute; it then overshot the other way to 1.5x too hard. The DAG widened accordingly: 3,681 blocks against 1,656 chain blocks.
Hash-rate profile derived from the record (expected hashes of every block over wall time, between the two visible events, Metal stop 19:28:05 UTC and PC start 19:42:40 UTC):
| From s | To s | Blocks/s at genesis difficulty | MH/s |
|---|---|---|---|
| 0 | 1,215 | 0.114 | 30.7 |
| 1,215 | 2,090 | 0.006 | 1.5 |
| 2,090 | 4,064 | 0.433 | 116.2 |
Exact replay check: the record's own timestamps and bits replayed through rusty-kaspa's integer
arithmetic (sim.py kaspa_bits_exact, same sampling rule (parent_daa + 1) mod 4 = 0, 661 samples,
earliest sample dropped from the average, compact-bits rounding) reproduce the chain's bits exactly
for the 244 retargets from DAA 600 to 844 and diverge from DAA 845, when the chain had passed
1 block/s and the sampled window began to include merged blocks that a chain-only replay cannot see.
The simulator's Kaspa model is therefore exact on a chain and approximate on a wide DAG.
3. Simulator
sim/difficulty/sim.py. One selected chain, DAA score = height, 1 block per second target,
solve times exponential with mean (expected hashes) / (hash rate); a hash-rate profile is piecewise
constant in time (miners) or keyed to the 3,600-block epoch (programs); timestamps are the true
times, optionally with uniform jitter. The difficulty unit is 2^28 hashes (the devnet genesis), so
every synthetic run starts at a realistic target far from MAX_DIFFICULTY_TARGET.
Metrics. "First within 10%": the first time after a step when the centred 121-block mean of the expected block rate (hash rate over difficulty) is within 10% of target. "Settled": the first time it then stays within 10% for 100 blocks. "Overshoot": how far past target the mean goes on the far side before the next step. "Blocks above 2x / below 0.5x": blocks produced while the expected rate was outside that band. "Worst gap": longest inter-block time. Steady state: standard deviation of the expected rate ratio over a steady hour ("steady std"), and the coefficient of variation of blocks per minute, which is 0.129 for a Poisson process at 60 per minute (the controller adds to it only when it is noisy).
Controllers.
| Name | Rule | Source |
|---|---|---|
| kaspa | KIP-4 sampled DAA as the fork runs it | consensus/src/processes/difficulty.rs, constants.rs (661 samples, every 4th block, min 150 samples) |
| monero | 720 blocks, sorted timestamps, 60 cut each side, lag 15 | Monero next_difficulty, approximate from memory of the reference |
| lwma60, lwma120 | Zawy LWMA-1: linear weights, average target, solvetimes in [-132 s, 6 T] | Zawy's 2018 reference, approximate from memory |
| igneum | the rule of spec 2.3 | this document |
| igneum-literal | the brief's trigger: short lane engaged while the short-window rate is more than 25% off target | kept to measure the chatter |
Profiles. up50 (x50 at 3 h), down50 (/50 at 3 h), epoch30 (x1.3 or /1.3 drawn at each epoch boundary from epoch 2, six boundaries), walk10 (10% per hour log random walk), hop3 and hop10 (a 3x or 10x pool in and out every 15 minutes, twelve steps), polluted (the devnet case: 21 minutes at 0.11 blocks/s, 13 minutes idle, then 0.6 blocks/s at genesis difficulty), warmup-hard (genesis 10x too hard), warmup-easy (genesis 10x too easy), real (the record's profile above).
4. Designing the Igneum candidate
Two findings changed the brief's sketch.
Zawy's LWMA estimator is biased while targets ramp. LWMA computes average target times average
solvetime. While the clamp drags the target down 3% per block after a 50x step, the short window
holds blocks mined at targets from 1x to 15x apart; average target times average solvetime then
over-estimates the target (Jensen), and the fast lane stalled at about 15x of the 50x step for a
full window length (trajectory dump, first implementation). Replacing every lane's estimator with
work over time (the sum of blue-work increments over the sum of clamped solvetimes, linear weights
for the short lane) removed the stall: the 50x step-up settles in 62 s instead of 224 s. This is the
estimator behind Kaspa's estimateNetworkHashesPerSecond, so the lineage stays Kaspa's.
The brief's trigger chatters. "Fast lane takes over when the short-window rate departs from target
by more than 25%" releases the fast lane as soon as the short window is back on target, while the
long window is still polluted by the pre-step blocks for 44 minutes; the long lane then pulls the
target the wrong way until the short window departs again. Measured as igneum-literal: polluted
case settles at 1,910 s against 72 s, hop3 fails to settle on 8 of 12 steps against 0, hop10 on 10
against 0. The trigger adopted compares the two lanes: the short lane rules while it disagrees with
the reference lane by more than 25%, which stays true exactly as long as the long window is wrong.
The epoch windows. At an epoch boundary the program changes and the hash rate steps; blocks of the old program carry no information about the new one. Every lane is therefore restricted to the block's own epoch: the short lane is the newest 120 blocks of the epoch, the reference lane is the epoch window (blended with the parent's implied rate as k : 16 for small k) until the epoch has 600 blocks and Kaspa's sampled window restricted to the epoch after that. The parent's target is held for the first 8 blocks of an epoch. Epoch 0 starts at genesis, so this is also the warm-up rule: the dead zone is 8 blocks, not 600.
Asymmetric clamps. A step down is the painful case for any past-only controller: after a 50x drop blocks arrive every 50 s and each one is the only evidence. With a symmetric 3% clamp the 50x step-down took 1,786 s with a 211 s worst gap. The clamps are not what bounds timestamp manipulation (the symmetric solvetime cap and the cancellation of a forged timestamp by the next honest block are), so the ease direction can be faster: 10% per block gives 1,164 s and a 65 s worst gap with no measurable change anywhere else, and widening the cap from 6 to 20 block times (section 5) brings it to 657 s and 35 s.
5. Tuning on the synthetic set
One parameter at a time, two sweeps. Columns per profile: settled s ("rec"), first within 10% s
("first"), steady std; down50 adds the worst gap; polluted adds the peak rate. clamp_pct in the
first sweep moves both clamps together (the ease clamp did not exist yet).
First sweep, around the brief's symmetric 3% clamp and the 6 T cap (ns 120, trigger 25%, clamp 3% both ways, prior 16):
Tune ns
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ns=60 | 72.8 | 72.8 | 0.069 | 1843.1 | 1843.1 | 0.069 | 114.2 | 127.0 | 80.2 | 0.067 | none | none | 0.089 | 0/0 | 301.6 | 251.0 | 0.069 | 56.4 | 56.4 | 0.083 | 4.4 |
| ns=120 | 63.0 | 63.0 | 0.040 | 1786.2 | 1786.2 | 0.040 | 211.2 | 155.9 | 99.0 | 0.033 | none | none | 0.061 | 0/0 | 275.6 | 275.6 | 0.040 | 69.3 | 69.3 | 0.048 | 5.8 |
| ns=180 | 63.3 | 63.3 | 0.035 | 2131.4 | 2131.4 | 0.035 | 125.1 | 156.3 | 99.0 | 0.017 | none | none | 0.056 | 0/0 | 356.1 | 356.1 | 0.035 | 76.5 | 76.5 | 0.042 | 5.6 |
Tune trig
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| trig=0.15 | 66.7 | 66.7 | 0.077 | 1591.5 | 1591.5 | 0.077 | 98.7 | 137.3 | 107.3 | 0.070 | none | none | 0.082 | 0/0 | 312.9 | 261.8 | 0.077 | 58.1 | 58.1 | 0.075 | 5.8 |
| trig=0.25 | 63.0 | 63.0 | 0.040 | 1786.2 | 1786.2 | 0.040 | 211.2 | 155.9 | 99.0 | 0.033 | none | none | 0.061 | 0/0 | 275.6 | 275.6 | 0.040 | 69.3 | 69.3 | 0.048 | 5.8 |
| trig=0.35 | 62.9 | 62.9 | 0.034 | 1835.4 | 1835.4 | 0.034 | 126.4 | 156.3 | 99.0 | 0.016 | none | none | 0.054 | 0/0 | 328.5 | 279.2 | 0.034 | 78.4 | 78.4 | 0.042 | 5.5 |
Tune clamp_pct
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| clamp_pct=0.02 | 84.0 | 84.0 | 0.039 | 2588.2 | 2588.2 | 0.039 | 216.8 | 156.3 | 99.4 | 0.031 | none | none | 0.060 | 0/0 | 373.6 | 336.9 | 0.039 | 80.1 | 80.1 | 0.046 | 5.4 |
| clamp_pct=0.03 | 63.0 | 63.0 | 0.040 | 1786.2 | 1786.2 | 0.040 | 211.2 | 155.9 | 99.0 | 0.033 | none | none | 0.061 | 0/0 | 275.6 | 275.6 | 0.040 | 69.3 | 69.3 | 0.048 | 5.8 |
| clamp_pct=0.05 | 62.2 | 62.2 | 0.040 | 1312.5 | 1312.5 | 0.040 | 173.8 | 155.8 | 98.9 | 0.034 | none | none | 0.062 | 0/0 | 284.6 | 258.8 | 0.040 | 59.9 | 59.9 | 0.049 | 5.4 |
Tune ease_pct
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ease_pct=0.03 | 63.0 | 63.0 | 0.040 | 1786.2 | 1786.2 | 0.040 | 211.2 | 155.9 | 99.0 | 0.033 | none | none | 0.061 | 0/0 | 275.6 | 275.6 | 0.040 | 69.3 | 69.3 | 0.048 | 5.8 |
| ease_pct=0.06 | 62.7 | 62.7 | 0.039 | 1282.5 | 1282.5 | 0.039 | 176.0 | 155.6 | 98.8 | 0.031 | none | none | 0.061 | 0/0 | 299.5 | 265.0 | 0.039 | 58.5 | 58.5 | 0.048 | 5.8 |
| ease_pct=0.1 | 62.2 | 62.2 | 0.039 | 1164.4 | 1164.4 | 0.039 | 65.3 | 155.5 | 98.8 | 0.030 | none | none | 0.062 | 0/0 | 253.4 | 253.4 | 0.039 | 71.7 | 71.7 | 0.048 | 6.2 |
Tune k0
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| k0=8 | 63.0 | 63.0 | 0.040 | 1786.6 | 1786.6 | 0.040 | 211.1 | 156.0 | 99.0 | 0.033 | none | none | 0.061 | 0/0 | 311.5 | 280.9 | 0.040 | 67.1 | 67.1 | 0.048 | 5.5 |
| k0=16 | 63.0 | 63.0 | 0.040 | 1786.2 | 1786.2 | 0.040 | 211.2 | 155.9 | 99.0 | 0.033 | none | none | 0.061 | 0/0 | 275.6 | 275.6 | 0.040 | 69.3 | 69.3 | 0.048 | 5.8 |
| k0=32 | 63.0 | 63.0 | 0.040 | 1786.9 | 1786.9 | 0.040 | 211.9 | 156.0 | 99.1 | 0.033 | none | none | 0.061 | 0/0 | 290.0 | 263.0 | 0.040 | 71.4 | 71.4 | 0.047 | 5.7 |
Tune cap
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cap=6 | 63.0 | 63.0 | 0.040 | 1786.2 | 1786.2 | 0.040 | 211.2 | 155.9 | 99.0 | 0.033 | none | none | 0.061 | 0/0 | 275.6 | 275.6 | 0.040 | 69.3 | 69.3 | 0.048 | 5.8 |
| cap=20 | 63.8 | 63.8 | 0.039 | 1703.7 | 1703.7 | 0.039 | 165.8 | 143.7 | 86.8 | 0.033 | none | none | 0.061 | 0/0 | 274.9 | 257.2 | 0.039 | 62.9 | 62.9 | 0.048 | 5.8 |
| cap=132 | 63.8 | 63.8 | 0.039 | 1649.6 | 1649.6 | 0.039 | 161.0 | 143.7 | 86.8 | 0.033 | none | none | 0.061 | 0/0 | 263.5 | 238.2 | 0.039 | 75.8 | 75.8 | 0.048 | 13.1 |
Second sweep, around the adopted defaults (ns 120, trigger 25%, harden 3%, ease 10%, prior 16, cap 6 T at the time of the sweep; the cap row decided the final value):
Tune ns
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ns=60 | 57.5 | 57.5 | 0.080 | 595.9 | 595.9 | 0.080 | 87.5 | 120.2 | 92.6 | 0.076 | none | none | 0.093 | 0/0 | 201.2 | 152.1 | 0.080 | 60.8 | 60.8 | 0.090 | 5.2 |
| ns=120 | 62.2 | 62.2 | 0.039 | 1164.4 | 1164.4 | 0.039 | 65.3 | 155.5 | 98.8 | 0.030 | none | none | 0.062 | 0/0 | 253.4 | 253.4 | 0.039 | 71.7 | 71.7 | 0.048 | 6.2 |
| ns=180 | 63.5 | 63.5 | 0.034 | 1742.3 | 1742.3 | 0.034 | 93.4 | 155.8 | 98.8 | 0.017 | none | none | 0.056 | 0/0 | 354.2 | 344.3 | 0.034 | 77.3 | 77.3 | 0.042 | 5.7 |
Tune trig
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| trig=0.15 | 63.2 | 63.2 | 0.078 | 1098.2 | 1098.2 | 0.078 | 75.1 | 136.1 | 106.1 | 0.072 | none | none | 0.083 | 0/0 | 244.8 | 234.9 | 0.078 | 62.1 | 62.1 | 0.075 | 5.2 |
| trig=0.25 | 62.2 | 62.2 | 0.039 | 1164.4 | 1164.4 | 0.039 | 65.3 | 155.5 | 98.8 | 0.030 | none | none | 0.062 | 0/0 | 253.4 | 253.4 | 0.039 | 71.7 | 71.7 | 0.048 | 6.2 |
| trig=0.35 | 63.5 | 63.5 | 0.034 | 1238.4 | 1238.4 | 0.034 | 75.5 | 155.8 | 98.8 | 0.016 | none | none | 0.054 | 0/0 | 276.3 | 263.9 | 0.034 | 63.1 | 63.1 | 0.042 | 5.8 |
Tune clamp_pct
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| clamp_pct=0.02 | 97.1 | 97.1 | 0.037 | 1091.3 | 1091.3 | 0.037 | 71.6 | 155.3 | 98.7 | 0.027 | none | none | 0.061 | 0/0 | 269.7 | 258.5 | 0.037 | 71.9 | 71.9 | 0.048 | 6.2 |
| clamp_pct=0.03 | 62.2 | 62.2 | 0.039 | 1164.4 | 1164.4 | 0.039 | 65.3 | 155.5 | 98.8 | 0.030 | none | none | 0.062 | 0/0 | 253.4 | 253.4 | 0.039 | 71.7 | 71.7 | 0.048 | 6.2 |
| clamp_pct=0.05 | 60.1 | 60.1 | 0.040 | 1371.6 | 1371.6 | 0.040 | 175.9 | 155.5 | 98.6 | 0.033 | none | none | 0.063 | 0/0 | 295.1 | 262.7 | 0.040 | 71.6 | 71.6 | 0.049 | 6.1 |
Tune ease_pct
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ease_pct=0.03 | 63.0 | 63.0 | 0.040 | 1786.2 | 1786.2 | 0.040 | 211.2 | 155.9 | 99.0 | 0.033 | none | none | 0.061 | 0/0 | 275.6 | 275.6 | 0.040 | 69.3 | 69.3 | 0.048 | 5.8 |
| ease_pct=0.06 | 62.7 | 62.7 | 0.039 | 1282.5 | 1282.5 | 0.039 | 176.0 | 155.6 | 98.8 | 0.031 | none | none | 0.061 | 0/0 | 299.5 | 265.0 | 0.039 | 58.5 | 58.5 | 0.048 | 5.8 |
| ease_pct=0.1 | 62.2 | 62.2 | 0.039 | 1164.4 | 1164.4 | 0.039 | 65.3 | 155.5 | 98.8 | 0.030 | none | none | 0.062 | 0/0 | 253.4 | 253.4 | 0.039 | 71.7 | 71.7 | 0.048 | 6.2 |
| ease_pct=0.15 | 61.6 | 61.6 | 0.038 | 1111.4 | 1111.4 | 0.038 | 65.4 | 155.2 | 98.4 | 0.030 | none | none | 0.062 | 0/0 | 261.6 | 250.5 | 0.038 | 67.1 | 67.1 | 0.048 | 6.4 |
Tune k0
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| k0=8 | 62.2 | 62.2 | 0.038 | 1144.7 | 1144.7 | 0.038 | 65.3 | 155.5 | 98.7 | 0.030 | none | none | 0.062 | 0/0 | 247.7 | 247.7 | 0.038 | 70.2 | 70.2 | 0.048 | 6.2 |
| k0=16 | 62.2 | 62.2 | 0.039 | 1164.4 | 1164.4 | 0.039 | 65.3 | 155.5 | 98.8 | 0.030 | none | none | 0.062 | 0/0 | 253.4 | 253.4 | 0.039 | 71.7 | 71.7 | 0.048 | 6.2 |
| k0=32 | 62.2 | 62.2 | 0.038 | 1169.9 | 1169.9 | 0.038 | 65.3 | 155.7 | 98.9 | 0.030 | none | none | 0.061 | 0/0 | 287.5 | 266.4 | 0.038 | 71.4 | 71.4 | 0.048 | 6.3 |
Tune cap
| ctrl | up50 rec | up50 first | up50 std | down50 rec | down50 first | down50 std | down50 gap | epoch30 rec | epoch30 first | epoch30 std | walk10 rec | walk10 first | walk10 std | walk10 >2x/<0.5x | hop10 rec | hop10 first | hop10 std | polluted rec | polluted first | polluted std | polluted peak |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cap=6 | 62.2 | 62.2 | 0.039 | 1164.4 | 1164.4 | 0.039 | 65.3 | 155.5 | 98.8 | 0.030 | none | none | 0.062 | 0/0 | 253.4 | 253.4 | 0.039 | 71.7 | 71.7 | 0.048 | 6.2 |
| cap=20 | 61.7 | 61.7 | 0.038 | 657.4 | 657.4 | 0.038 | 35.3 | 143.7 | 86.5 | 0.030 | none | none | 0.062 | 0/0 | 211.9 | 208.1 | 0.038 | 70.0 | 70.0 | 0.049 | 8.5 |
| cap=132 | 61.7 | 61.7 | 0.038 | 629.5 | 629.5 | 0.038 | 32.4 | 143.7 | 86.5 | 0.030 | none | none | 0.062 | 0/0 | 212.9 | 209.1 | 0.038 | 63.8 | 63.8 | 0.049 | 16.4 |
Decisions: ns 120 (60 doubles the steady noise, 180 is slower on hops); trigger 25% (15% engages on noise and doubles the steady noise, 35% slower on hops); harden 3% (2% slows the step-up to 97 s, 5% buys nothing on the up step and worsens the down step's gap); ease 10% (3% leaves the step-down at 1,786 s, 15% buys 50 s more at no cost but was not adopted to keep a margin); prior 16 (no effect across 8 to 32); cap 20 T (6 T leaves the step-down at 1,164 s, 20 T gives 657 s and a 35 s worst gap and also helps the epoch steps and hopping; the 132 s cap, Kaspa's future tolerance, gains nothing more and lets the idle gap of the polluted case over-ease the target to a 16x peak against 8.5x at 20 T and 6.2x at 6 T). The cap is not the manipulation bound: under the symmetric cap a forged timestamp's contribution is cancelled by the next honest block whatever the cap, so the cap only decides how much a genuine slow block may say. Nothing was tuned on the record.
6. Results on the synthetic set
Seed 7, honest timestamps, final parameters. Full output in sim/difficulty/results.md.
up50: hash rate x50 at 3 h
| controller | first within 10% s | settled s | recovery blocks | overshoot | steady std of rate | steady blocks/min CV | worst gap s | blocks above 2x | blocks below 0.5x | blocks | fast-lane blocks |
|---|---|---|---|---|---|---|---|---|---|---|---|
| kaspa | 1542.1 | 1542.1 | 4967 | 0.009 | 0.012 | 0.126 | 9.5 | 3760 | 0 | 21511 | none |
| monero | 94.0 | 94.0 | 713 | 0.089 | 0.037 | 0.144 | 10.3 | 657 | 0 | 18490 | none |
| lwma60 | 105.0 | 105.0 | 182 | 0.208 | 0.131 | 0.088 | 11.3 | 78 | 0 | 17977 | none |
| lwma120 | 231.2 | 231.2 | 388 | 0.189 | 0.092 | 0.096 | 10.5 | 164 | 0 | 18078 | none |
| igneum | 61.7 | 61.7 | 181 | 0.140 | 0.038 | 0.135 | 11.3 | 125 | 0 | 17927 | 377 |
| igneum-literal | 63.7 | 63.7 | 186 | 0.217 | 0.034 | 0.130 | 11.3 | 127 | 0 | 17956 | 230 |
down50: hash rate /50 at 3 h
| controller | first within 10% s | settled s | recovery blocks | overshoot | steady std of rate | steady blocks/min CV | worst gap s | blocks above 2x | blocks below 0.5x | blocks | fast-lane blocks |
|---|---|---|---|---|---|---|---|---|---|---|---|
| kaspa | 12295.7 | 12295.7 | 1569 | 5.102 | 0.012 | 0.126 | 178.5 | 1648 | 1256 | 16185 | none |
| monero | 6432.8 | 6432.8 | 701 | 0.039 | 0.037 | 0.144 | 187.3 | 0 | 567 | 19356 | none |
| lwma60 | 577.9 | 577.9 | 111 | 0.154 | 0.131 | 0.088 | 118.7 | 0 | 55 | 24588 | none |
| lwma120 | 1073.6 | 1073.6 | 177 | 0.129 | 0.092 | 0.096 | 64.9 | 0 | 97 | 24205 | none |
| igneum | 657.4 | 657.4 | 147 | 0.062 | 0.038 | 0.135 | 35.3 | 0 | 90 | 24548 | 474 |
| igneum-literal | 967.0 | 967.0 | 146 | 0.062 | 0.034 | 0.130 | 121.1 | 0 | 86 | 24276 | 267 |
epoch30: x1.3 or /1.3 at each epoch boundary from epoch 2
| controller | first within 10% s | settled s | worst recovery s | not recovered | overshoot | steady std of rate | steady blocks/min CV | worst gap s | blocks above 2x | blocks below 0.5x | blocks | fast-lane blocks |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| kaspa | 1583.0 | 1583.0 | 1974.6 | 0 | 0.043 | 0.010 | 0.130 | 12.3 | 0 | 0 | 29403 | none |
| monero | 456.2 | 456.2 | 603.3 | 0 | 0.116 | 0.038 | 0.141 | 16.0 | 0 | 0 | 28857 | none |
| lwma60 | 91.1 | 156.9 | 310.6 | 0 | 0.291 | 0.132 | 0.090 | 11.6 | 0 | 0 | 28658 | none |
| lwma120 | 136.9 | 153.1 | 240.8 | 0 | 0.229 | 0.094 | 0.097 | 11.5 | 0 | 0 | 28719 | none |
| igneum | 86.5 | 143.7 | 260.8 | 0 | 0.126 | 0.030 | 0.133 | 11.3 | 0 | 0 | 28735 | 351 |
| igneum-literal | 86.4 | 143.8 | 260.8 | 0 | 0.050 | 0.018 | 0.130 | 11.3 | 0 | 0 | 28775 | 149 |
walk10: 10% per hour log random walk
| controller | steady std of rate | steady blocks/min CV | worst gap s | blocks above 2x | blocks below 0.5x | blocks | fast-lane blocks |
|---|---|---|---|---|---|---|---|
| kaspa | 0.049 | 0.144 | 11.1 | 0 | 0 | 21728 | none |
| monero | 0.049 | 0.143 | 10.6 | 0 | 0 | 21541 | none |
| lwma60 | 0.132 | 0.088 | 11.1 | 0 | 0 | 21486 | none |
| lwma120 | 0.093 | 0.105 | 10.5 | 0 | 0 | 21518 | none |
| igneum | 0.062 | 0.130 | 11.2 | 0 | 0 | 21521 | 519 |
| igneum-literal | 0.055 | 0.136 | 11.1 | 0 | 0 | 21523 | 184 |
hop3: 3x pool hops in and out every 15 min from 3 h
| controller | first within 10% s | settled s | worst recovery s | not recovered | overshoot | steady std of rate | steady blocks/min CV | worst gap s | blocks above 2x | blocks below 0.5x | blocks | fast-lane blocks |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| kaspa | none | none | none | 12 | 0 | 0.012 | 0.126 | 21.0 | 1208 | 1036 | 22128 | none |
| monero | 336.7 | 336.7 | 362.4 | 6 | 0.062 | 0.037 | 0.144 | 18.6 | 2438 | 1437 | 20382 | none |
| lwma60 | 109.5 | 166.9 | 373.4 | 0 | 0.270 | 0.131 | 0.088 | 14.7 | 111 | 42 | 21408 | none |
| lwma120 | 161.8 | 184.3 | 292.1 | 0 | 0.201 | 0.092 | 0.096 | 15.0 | 215 | 129 | 21364 | none |
| igneum | 124.0 | 189.5 | 410.3 | 0 | 0.229 | 0.038 | 0.135 | 11.8 | 213 | 182 | 21228 | 8054 |
| igneum-literal | 429.3 | 372.9 | 493.1 | 8 | 0.202 | 0.034 | 0.130 | 16.1 | 86 | 529 | 20913 | 4294 |
hop10: 10x pool hops in and out every 15 min from 3 h
| controller | first within 10% s | settled s | worst recovery s | not recovered | overshoot | steady std of rate | steady blocks/min CV | worst gap s | blocks above 2x | blocks below 0.5x | blocks | fast-lane blocks |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| kaspa | none | none | none | 12 | 0 | 0.012 | 0.126 | 52.1 | 5700 | 1035 | 22951 | none |
| monero | 283.5 | 283.5 | 312.9 | 6 | 0.085 | 0.037 | 0.144 | 83.2 | 3551 | 569 | 19203 | none |
| lwma60 | 165.5 | 263.9 | 542.0 | 0 | 0.242 | 0.131 | 0.088 | 34.3 | 315 | 198 | 21008 | none |
| lwma120 | 282.8 | 325.5 | 789.0 | 0 | 0.199 | 0.092 | 0.096 | 30.2 | 576 | 417 | 20676 | none |
| igneum | 208.1 | 211.9 | 382.9 | 0 | 0.230 | 0.038 | 0.135 | 25.0 | 422 | 321 | 20631 | 8445 |
| igneum-literal | 466.6 | 505.8 | 698.0 | 6 | 0.237 | 0.034 | 0.130 | 35.7 | 816 | 666 | 20683 | 5432 |
polluted: window polluted by a pre-step slow period (21 min at 0.11 blocks/s, 13 min idle, then 0.6 blocks/s at genesis difficulty)
| controller | first within 10% s | settled s | recovery blocks | overshoot | steady std of rate | steady blocks/min CV | worst gap s | blocks above 2x | blocks below 0.5x | blocks | fast-lane blocks | peak rate | trough rate after 10 min |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| kaspa | 650.7 | 2748.3 | 5163 | 6.942 | 0.010 | 0.134 | 11.2 | 2320 | 0 | 18592 | none | 7.9 | 0.6 |
| monero | 124.0 | 124.0 | 735 | 0.092 | 0.032 | 0.142 | 10.4 | 675 | 0 | 17641 | none | 15.0 | 0.9 |
| lwma60 | 66.1 | 66.1 | 113 | 0.183 | 0.132 | 0.089 | 11.3 | 36 | 0 | 17138 | none | 5.7 | 0.6 |
| lwma120 | 110.3 | 110.3 | 201 | 0.147 | 0.091 | 0.108 | 10.5 | 74 | 0 | 17190 | none | 6.0 | 0.7 |
| igneum | 70.0 | 70.0 | 136 | 0.180 | 0.049 | 0.122 | 11.3 | 72 | 0 | 17005 | 2585 | 8.5 | 0.6 |
| igneum-literal | 817.0 | 1876.4 | 2546 | 0.197 | 0.040 | 0.141 | 11.3 | 571 | 0 | 17618 | 1986 | 8.4 | 0.6 |
warmup-hard: genesis difficulty 10x too hard
| controller | first within 10% s | settled s | recovery blocks | overshoot | steady std of rate | steady blocks/min CV | worst gap s | blocks above 2x | blocks below 0.5x | blocks | fast-lane blocks |
|---|---|---|---|---|---|---|---|---|---|---|---|
| kaspa | 5790.7 | none | none | 92.876 | 32.727 | 3.002 | 67.2 | 4860 | 599 | 6444 | none |
| monero | 286.6 | 286.6 | 159 | 0.047 | 0.038 | 0.135 | 17.5 | 0 | 33 | 7011 | none |
| lwma60 | 155.2 | 155.2 | 118 | 0.147 | 0.132 | 0.078 | 10.5 | 0 | 18 | 7128 | none |
| lwma120 | 187.9 | 187.9 | 152 | 0.111 | 0.095 | 0.102 | 10.5 | 0 | 18 | 7128 | none |
| igneum | 321.9 | 321.9 | 199 | 0.024 | 0.031 | 0.126 | 10.5 | 0 | 23 | 6953 | 138 |
| igneum-literal | 351.4 | 702.7 | 568 | 0.024 | 0.019 | 0.134 | 10.5 | 0 | 23 | 6979 | 179 |
warmup-easy: genesis difficulty 10x too easy
| controller | first within 10% s | settled s | recovery blocks | overshoot | steady std of rate | steady blocks/min CV | worst gap s | blocks above 2x | blocks below 0.5x | blocks | fast-lane blocks |
|---|---|---|---|---|---|---|---|---|---|---|---|
| kaspa | 1554.8 | 1554.8 | 3168 | 0.011 | 0.008 | 0.137 | 9.8 | 599 | 0 | 8869 | none |
| monero | 95.4 | 95.4 | 76 | 0.091 | 0.038 | 0.154 | 16.1 | 16 | 16 | 7145 | none |
| lwma60 | 57.9 | 57.9 | 87 | 0.183 | 0.133 | 0.073 | 9.2 | 8 | 0 | 7192 | none |
| lwma120 | 88.3 | 88.3 | 136 | 0.147 | 0.096 | 0.099 | 9.4 | 8 | 0 | 7225 | none |
| igneum | 58.7 | 58.7 | 114 | 0.082 | 0.029 | 0.133 | 10.3 | 61 | 0 | 7192 | 120 |
| igneum-literal | 58.4 | 58.4 | 114 | 0.086 | 0.015 | 0.129 | 10.3 | 61 | 0 | 7209 | 191 |
With +-500 ms timestamp jitter (half a block time, a harsh clock model), the ordering holds: up50 settled Kaspa 1,535 s, LWMA60 159 s, LWMA120 140 s, Igneum 169 s; down50 Kaspa 12,523 s, LWMA60 847 s, LWMA120 966 s, Igneum 1,047 s (first within 10% at 896 s, worst gap 49 s against 78 and 81); epoch30 Kaspa 1,535 s, LWMA60 63 s, LWMA120 124 s, Igneum 55 s; hop10 Kaspa never, LWMA60 210 s, LWMA120 318 s, Igneum 228 s; polluted Kaspa 2,722 s (peak 8.5x), LWMA60 89 s, LWMA120 174 s, Igneum 71 s; steady std Kaspa 0.010, LWMA60 0.125, LWMA120 0.085, Igneum 0.046. The jitter costs the Igneum short lane on the 50x cases because 20 ms solve times are drowned by 500 ms clock noise; it still beats Kaspa by 9x and 12x there.
Reading. Kaspa's rule has the lowest steady-state noise (0.012) and the worst response to every step: 26 minutes for a 50x up, 3.4 hours for a 50x down with a 5x overshoot, never settling under pool hopping, 46 minutes and a 7.9x peak on the devnet case, and a 600-block dead zone that turns a 10x-too-hard genesis into 97 minutes of near-silence. LWMA 60 is the fastest on step-downs and the noisiest (0.132, 13% block-rate swings all day). The Igneum rule keeps the steady noise at 0.038 (three times Kaspa's, less than half of LWMA120's), is the fastest on the 50x step-up (62 s), the epoch steps (144 s settled, 87 s first within 10%, against Kaspa's 1,583 s), the devnet case (70 s), the 3x and 10x hopping (190 s and 212 s) and the too-easy genesis (59 s), second to LWMA60 on the 50x step-down (657 s against 578 s, with a 35 s worst gap against 119 s) and on the too-hard genesis (322 s against 155 s, the price of the 10% ease clamp). On the polluted case every past-only controller shows a peak when the PC arrives, because the difficulty was correctly set for the Metal miner; the number that matters is how long the peak lasts: Igneum 70 s, Kaspa 46 minutes with a second, larger peak of its own making.
7. Validation on the record
The record's hash-rate profile (section 2) replayed through every controller, not tuned on it:
| Controller | First within 10% s | Settled s | Overshoot | Blocks above 2x | Blocks within 10% of target (of about 3,700) | Fast-lane blocks |
|---|---|---|---|---|---|---|
| kaspa | 262 | never | 6.24 (peak 7.6x) | 2,340 | 0 | |
| monero | 136 | 136 | 0.09 | 672 | 2,822 | |
| lwma60 | 75 | 75 | 0.18 | 87 | 2,175 | |
| lwma120 | 157 | 157 | 0.15 | 174 | 2,390 | |
| igneum | 79 | 79 | 0.18 | 132 | 2,608 | 2,484 |
| igneum-literal | 482 | 1,516 | 0.01 | 515 | 2,170 | 1,959 |
The simulated Kaspa trajectory reproduces the live one: a late first retarget, a multi-x peak, thousands of blocks above 2x, no settling within the record. The Igneum rule settles 79 s after the PC arrives; it runs on the short lane for most of the remaining 33 minutes (2,484 blocks) because the long window stays polluted by the 75x step for 44 minutes, which is the designed behaviour.
8. Implementation
Branch difficulty of vendor/igneum-node (worktree vendor/igneum-node-diff).
| File | Change |
|---|---|
consensus/core/src/igneum.rs |
DifficultyRule { KaspaSampled, IgneumDual } (serde kebab-case) and the difficulty constants module (SHORT_WINDOW 120, EPOCH_HOLD 8, PRIOR_BLOCKS 16, LONG_MIN 600, TRIGGER_PERCENT 25, HARDEN_PERCENT 3, EASE_PERCENT 10, CAP_BLOCKS 20) |
consensus/core/src/config/params.rs |
Params.difficulty_rule (IgneumDual on all four networks), OverrideParams.difficulty_rule and OverrideParams.genesis_bits (test networks: the genesis hash is recomputed from the new bits) |
consensus/src/processes/difficulty.rs |
calculate_difficulty_bits(window, ghostdag, daa_score) dispatches on the rule; kaspa_difficulty_bits is the old body unchanged; igneum_difficulty_bits gathers the chain steps (blue-work increment and clamped solvetime per selected-chain block of the epoch, at most 600 or 120) and the in-epoch samples of the window, then calls the pure igneum_target (Uint320 arithmetic, rates as unreduced fractions, no floating point) |
consensus/src/processes/window.rs, consensus/src/consensus/services.rs |
the rule and the epoch length threaded to the manager; the DAA score of the block passed to the retarget |
testing/integration/src/consensus_integration_tests.rs |
difficulty_test pins KaspaSampled (it asserts Kaspa's behaviour on mainnet parameters) |
Unit tests (cargo test -p kaspa-consensus --lib difficulty, 9 tests): hold for the first 8
blocks, steady state keeps the target within 0.1%, a 50x step up hardens by exactly 3% per block,
a 50x step down eases by exactly 10% per block, the short lane takes over only beyond 25%
disagreement with the long lane (on target and 20% fast stay on the long lane, 2x fast binds the
3% clamp), the epoch lane's shrinkage (8 blocks at 10% slower move the target 3.1%, 8 blocks at half
speed bind the 10% clamp), the maximum target is never exceeded, plus Kaspa's two level-work tests.
cargo test -p kaspa-consensus-core --lib params: 4 tests including the override-file parsing.
Cost: up to 600 compact-header and ghostdag reads per header for the first 600 blocks of an epoch, 120 afterwards, all from in-memory caches at 1 BPS. Re-measure before the 4 BPS step.
9. Test network
Three igneumd nodes from the difficulty branch (gRPC 26800, 26810, 26820; P2P 26801, 26811, 26821; node 2 and node 3 --connect to node 1; appdir /tmp/igneum-diff-test/igneum; override file {"difficulty_rule": "igneum-dual", "genesis_bits": 505413632}, i.e. bits 0x1e200000, 2^19 expected hashes, genesis hash f682b78a...e099), three igneum-miner mine --engine igneum-pow CPU miners of 4 threads each with their own payout identities, on the shared Mac at load average 50 to 90 (two other agents were building). Schedule: miner A on node 1 for 1,200 s from block 1; miners B and C on nodes 2 and 3 from 359 s to 779 s. 1,133 blocks, 0 rejected (814 + 159 + 160 found), one sink at the end. Record: sim/difficulty/testnet-igneum-2026-10-03.csv.
What the miners delivered (expected hashes over wall time, the same estimator the simulator's record profile uses): 0.0653 MH/s with A alone, 0.0988 MH/s with A, B and C (1.51x, not 3x: three 4-thread miners on a loaded 18-core Mac share the cores, B and C each reported 0.032 MH/s against A's 0.054), 0.0686 MH/s after B and C stopped (0.69x). Genesis difficulty 262,144 was 4.0x too hard for A alone (the correct value is about 65,000).
| Event | Measured, first within 10% (61-block mean) | Simulator prediction on the same profile, 5 seeds | Kaspa's rule, simulator |
|---|---|---|---|
| Warm-up from a 4x too hard genesis | 132 s (held 8 blocks, then eased 10% per block; difficulty 190,283 at 60 s, 62,248 at 90 s) | 3, 92, 263, 278, 309 s (median 263) | never within 20 min (no retarget before block 600; 337 to 378 blocks in 20 min) |
| Miners B and C join, x1.51 | 214 s (within 10% at 23 s, then a 1.9 blocks/s burst at 360 to 390 s as the DAA score jumped with the merged blocks) | 49, 54, 61, 87, 180 s (median 61) | never |
| Miners B and C leave, /1.45 | 85 s | 110, 231, 300 s and twice not within the remaining 413 s | never |
| Worst gap after the warm-up | 7.3 s | 4.5 to 13.0 s | 13 to 21 s |
Reading: the live network recovered faster than the simulator on the warm-up and the step-down and slower on the step-up. The differences have a cause the simulator does not model: the measured hash rate of CPU miners on a loaded machine swings by 2x between 30-second bins (0.03 to 0.15 MH/s in the table above), so a 1.5x step is of the same size as the noise, and the 61-block mean crosses the 10% band on noise as often as on control. The warm-up, the one clean signal (4x), came in at 132 s against a simulator median of 263 s, both an order of magnitude inside Kaspa's 600-block dead zone (which at 0.25 blocks/s would have lasted 40 minutes). Kaspa's rule, live, on the same network and genesis (sim/difficulty/testnet-kaspa-2026-10-03.csv, override {"difficulty_rule": "kaspa-sampled", "genesis_bits": 505413632}, 10 minutes, A for 600 s, B and C from 200 s to 500 s): 70 blocks, bits 0x1e200000 on all 70 (difficulty 262,144 never moved), 0.08 blocks/s, worst gap 63 s, delivered hash rate 0.042 MH/s (the other agents' builds were heavier during this run). Never within 10% of target at any point: the 600-block dead zone would have ended after about 2 hours at that rate. The Igneum run on the same genesis was within 10% at 132 s.
Per-30-second table (blocks, mean difficulty, work over time) in sim/difficulty/results.md.
10. Limits and what is still open
The simulator has no DAG: blocks off the selected chain enter the implementation only through blue work, and the record replay is chain-only (exact for 244 retargets, approximate after). No network latency, no template refresh delay. Monero and LWMA are reproduced from memory of their reference code and labelled approximate. The step-down remains the slowest case (657 s for 50x); real-time targeting would cut it further and is left out on purpose (spec 2.3). Red blocks' work is ignored by every lane, as by Kaspa's estimator. The 4 BPS and 10 BPS steps need the sampled window to replace the chain walk for the epoch lane and a re-derivation of the block-count constants.