igneum/docs/analysis/difficulty-2026-10-03.md
igneum-labs 2622fd2ccb Difficulty: timestamp attack fixed in spec 2.3, simulator, analysis addendum, ledger M23
Spec 2.3: rule 1 measures every chain step on a sanitised clock stored per header
(c(b) = max(c(p) + clamp(t(b) - c(p), -20 T, +20 T), t(b) - 60 T), step min(c(b) - c(p),
20 T)); rule 4 bounds the output to [2^128, MAX_DIFFICULTY_TARGET]; the timestamp rules
are Igneum's own, 10 s ahead of the clock and 10 s behind the selected parent beside the
unchanged past-median rule; new parameter rows, the bounds paragraph rewritten (the old
"next honest block cancels it" was the attack), the attack and test-network results added.

sim/difficulty/sim.py: class Igneum carries the same clock, lag bound and floor, so the
rule as simulated is the rule as coded (attacks.py's igneum-san is now identical to it).

docs/analysis/difficulty-2026-10-03.md section 11: the attack, the three parts, before and
after tables (simulator seeds 7 to 9, base-profile regression within 10% on the 3-seed
means, pool hopping unchanged, the two 15-minute 3-node forger runs), unit tests, limits.

docs/bench-log.md: the 4 October entry. docs/fud-ledger.md: M23, status Fixed.

Node side: vendor/igneum-node branch difficulty, commit 52eacad9.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 00:56:20 +00:00

48 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.

11. Addendum, 4 October 2026: the timestamp attack and the fix

The attack run of sim/difficulty/attacks/README.md (consensus test engineer, 4 October 2026) failed the rule of sections 4 to 9 on one scenario and found one panic. Timestamp stretching: a forger at 30 or 50% of the hash rate stamping every block at the edge of Kaspa's rules (132 s ahead, or the past median plus one) took the simulated block rate to 0.66 / 0.42 / 0.51 of target at 30% (latest / earliest / alternating) and 0.56 / 0.12 / 0.23 at 50%, difficulty 1.5x to 9.9x on an unchanged hash rate; on a 3-node test network a 50% forger took the chain to 0.24 blocks/s at 3x difficulty (earliest) and 0.59 at 1.9x (latest). The cause was the sentence in spec 2.3 that was meant as the defence: the symmetric 20 T clamp cancelled every forged step against the honest step after it, so the lanes measured 1 - 2a(1 - a) of real time at forger share a. Block flood: 85 blocks per second of PoW-less input drove the target below 2^64 after 4,142 blocks and calc_work panicked.

The three parts the README proposed are now the rule (difficulty branch, commit "Difficulty: timestamp rules 10 s both ways, sanitised clock per header, target floor 2^128"):

Part Rule Where
A a header is at most 10 s ahead of the node's clock (FUTURE_TOLERANCE_MS) and at least its selected parent's timestamp minus 10 s (BACK_TOLERANCE_MS), beside the unchanged past-median rule; the template floor is the same pre_ghostdag_validation.rs, post_pow_validation.rs (new error TimeTooFarBehindParent), virtual_processor/processor.rs
B every validated header gets a sanitised clock c(b) = max(c(p) + clamp(t(b) - c(p), -20 T, +20 T), t(b) - 60 T), stored per header (stores::clock, prefix 62, deleted at pruning); the short and epoch lanes measure clock steps min(c(b) - c(p), 20 T); the long lane keeps the raw sampled span igneum.rs (sanitised_clock, clock_step), header_processor/processor.rs, difficulty.rs (the chain walk)
C both rule paths bound the output to [2^128, MAX_DIFFICULTY_TARGET] (bound_target) difficulty.rs

The simulator (sim.py, class Igneum) carries the same clock and floor, so the rule as simulated is the rule as coded; attacks.py's candidate igneum-san is now identical to igneum.

Before and after

Timestamp stretching, simulator, seeds 7 to 9, block rate after one hour of forging as a fraction of target (mean; worst seed in brackets), 6-hour runs (attacks.py --scenario ts):

Forger Stamp 3 Oct rule, Kaspa's 132 s bounds 3 Oct rule, 10 s bounds alone 4 Oct rule (bounds and clock)
30% latest 0.66 1.008 1.008 (1.018)
30% earliest 0.42 0.636 (0.619) 1.008 (1.024)
30% alternating 0.51 0.975 1.004 (1.012)
50% latest 0.56 1.009 1.009 (1.021)
50% earliest 0.12 0.170 (0.150) 1.007 (1.027)
50% alternating 0.23 0.949 1.011 (1.022)
Mean difficulty ratio, worst cell 9.9x 5.9x 1.00
Worst gap, worst cell 234 s 106 s 10.1 s
Flood, 85 blocks/s, 6,000 blocks panic at block 4,142 panic floor 2^128 reached at block 2,635 (simulator) and about 2,630 (unit test), no panic

The middle column is from the README's "tight rules alone" run; the clock alone under Kaspa's bounds still collapsed at 50% (the clock's lag is a martingale once the forgery range exceeds half the cap), which is why both parts ship. Kaspa's rule under the 10 s bounds drifts +0.0% to +0.9%.

Base profiles, simulator, seeds 7 to 9, 3-seed means (settled = 121-block mean within 10% of target for 100 blocks; the regression criterion was 10% of the 3 October numbers):

Profile 3 Oct rule 4 Oct rule Change
Devnet record, 75x step, settled s 102.8 (78.7, 151.3, 78.5) 91.0 (73.1, 121.1, 78.9) -11% (faster)
Devnet record, first within 10% s 70.3 68.5 -3%
up50, settled s 154.4 154.4 0
down50, settled s 782.3 762.2 -3%
down50, worst gap s 78.3 73.9 -6%
epoch30, settled s (mean of the steps) 87.6 87.6 0
hop10, settled s 239.4 245.1 +2%
polluted, settled s 70.1 (70.0, 61.5, 78.9) 74.9 (62.7, 70.0, 92.0) +7% (seed 9: +17%)
polluted, peak rate 8.0x 11.4x the price of the 60 T lag bound (no bound: 275x)
Steady std of the block rate 0.038 to 0.045 unchanged 0

With honest stamps the sanitised clock is the raw clock except after an idle gap, which is where the two profiles that contain one (down50, polluted, the record) move: a gap longer than 60 T is paid back as 20 T steps for three blocks instead of being clamped once, so the rule eases a little faster after a gap (down50, the record) and overshoots a little more on the polluted window.

Pool hopping, simulator, 24 h, hopper's blocks per hash against the always-on base (attacks.py --scenario hop, seeds 7 to 9): unchanged to three decimals, as it must be with honest stamps. Greedy hopper +1.5% at 10% of the base, +1.0% at 30%, +0.9% at 50%, +0.7% at 100%; with a 60 s minimum dwell +1.3%, +0.1%, -1.7%, -4.0%; on 8 to 44% of the time, 227 to 818 switches a day; Kaspa's rule +0.4% to +0.8%. Under 5% in every cell; the 0.7-point excess over Kaspa's rule stays as the README left it.

Test network, 3 igneumd nodes from the fix plus the attack hook of diff-attacks (the forger's node shifts its template stamps; validation is the fixed rule on every node), genesis bits 0x1f010000, honest 4-thread miner A on node 1 for 15 minutes, forger F (4 threads, about 50%) honest on node 2 for 5 minutes then on node 3 with the offset, ports 28500 to 28521, appdir /tmp/igneum-diff-fix (records ts-past-igneum/record.csv, ts-future-igneum/record.csv there):

Run (15 min, forging from 300 s) Phase Chain blocks/s All blocks/s Mean difficulty Forger share Forger stamp offset s (mean; min, max) Delivered hash
4 Oct rule, earliest allowed stamp (986 blocks, 823 chain, 0 rejected) honest 60 to 300 s 0.82 1.00 102,226 0.48 +3 (-15, +10) A 0.110 MH/s over the run, F 0.109 MH/s over its forging leg
forging 300 to 900 s 0.88 1.02 100,134 0.48 -23 (-90, -10) same
last 300 s 0.83 0.95 106,141 0.47 -24 (-90, -10) same
4 Oct rule, latest allowed stamp, +9 s (1,028 blocks, 829 chain, 0 rejected) honest 60 to 300 s 0.78 0.95 102,382 0.41 +2 (0, +11) A 0.112 MH/s, F 0.111 MH/s
forging 300 to 900 s 0.89 1.09 96,717 0.52 +13 (+9, +26) same
last 300 s 0.89 1.11 102,754 0.54 +12 (+9, +26) same
3 Oct rule, earliest (README, ts-past-igneum.csv) honest 0 to 300 s 0.97 91,375 0.49 +2 A 0.078, F 0.069 MH/s
forging 300 to 900 s 0.24 275,135 0.53 -322 (to -566) same
last 300 s 0.20 341,191 0.59 -471 same
3 Oct rule, latest, +130 s (README, ts-future-igneum.csv) honest 0 to 300 s 0.98 89,363 0.51 +3 A 0.112, F 0.110 MH/s
forging 300 to 900 s 0.59 170,222 0.52 +134 same
last 300 s 0.50 191,259 0.50 +135 same

Chain blocks/s is the README's metric (every chain block placed at the stamp of the nearest earlier block of miner A, whose node keeps the real clock); with two miners on different nodes 16 to 19% of the blocks are merged rather than chained, so the all-blocks column (blocks whose DAA score falls inside the phase's chain range, over the phase length) is the rate the DAA sees. The honest phase is taken from 60 s because the genesis (2.5x too easy for two miners) gives 68 blocks in the first 30 s. Under the fixed rule the block rate is flat across the forging leg within the noise of two 4-thread CPU miners on a loaded Mac (chain 0.82 to 0.88 and 0.78 to 0.89, all-blocks 1.00 to 1.02 and 0.95 to 1.09), difficulty stays within 6% of the honest level (100k to 106k against 102k) on an unchanged delivered hash rate, and the honest nodes rejected nothing. The forger's offsets are what the rules allow: 10 s behind its parent, compounding to -90 s over a run of its own blocks before the past median stops it; 9 s ahead of its clock. Before the fix the same schedule took the chain to 0.24 blocks/s at 3x difficulty and 0.59 at 1.9x.

Unit tests

cargo test --release -p kaspa-consensus --lib difficulty: 12 pass, the nine of section 8 plus igneum_flood_at_85_blocks_per_second_stops_at_the_minimum_target (the should_panic test of diff-attacks with the panic removed: every output at or above 2^128, the floor reached between blocks 2,000 and 3,000, the floored work under 2^129), both_rules_share_the_target_floor and igneum_clock_steps_pay_a_forgery_back (three blocks stamped 10 s behind their parents followed by honest blocks: clock steps sum to the 8 s real span, raw clamped solvetimes sum to -6 s). cargo test --release -p kaspa-consensus-core --lib igneum: 9 pass, including sanitised_clock_telescopes_a_forged_stamp (a +10 s stamp and the honest block after it sum to real time; steps clamp at 20 s both ways; a 300 s idle gap jumps the clock to the stamp minus 60 s).

Limits

The clock store adds 8 bytes plus key per header. A header whose selected parent has no stored clock (the pruning point after a proof, trusted headers) starts the clock at the parent's raw stamp, so a forger could bias at most one window after a sync; not measured. The DAG effect of forged stamps on red and merged blocks is still unmeasured (one chain in the simulator). Kaspa's integration test difficulty_test pins KaspaSampled and the upstream timestamp tests assume the 132 s bounds; they were not re-run here. The hopper's 0.7-point excess over Kaspa's rule (README scenario 1) is unchanged by this fix and stays open.