Post-IIP-0120 LWMA retrospective — what the chain stall taught us
The 2026-05-04 chain stall at h=8614 ran for ~7 hours of real time before the v1.0.6.1 IIP-0120 fix landed and unblocked it. Posting this for miners who weren’t watching the incident in real time, plus anyone curious about how LWMA difficulty adjustment behaves at INTcoin’s hashrate scale.
Timeline
- 2026-05-03 05:57Z — community miner
int1qzf0cd…produces 23 blocks in 2 minutes (network hashrate momentarily spikes to ~300 KH/s). - 2026-05-03–04 — burst miner offline. Fleet hashrate drops back to ~1 KH/s steady state. LWMA still averaging the burst’s near-zero solvetimes against window-60.
- 2026-05-04 ~22:00Z — block 8615 lands after multi-minute gap. LWMA raises difficulty ~8% (the asymmetric clamp bug pinned the weighted average below T).
- 2026-05-04 22:00Z – 2026-05-05 03:30Z — chain stuck. 30+ mining threads across the fleet at the elevated difficulty produce zero blocks.
- 2026-05-05 03:30Z — v1.0.6 ships with IIP-0117/0118/0119/0120 fixes. But IIP-0120 wiring is dead-coded — the rule never fires.
- 2026-05-05 ~04:00Z — chain still at h=8615 after fleet upgrade. v1.0.6.1 wires IIP-0120; chain unblocks within minutes.
What LWMA was doing wrong (pre-fix)
INTcoin’s LWMA is a weighted moving average over the past N blocks (was 60, now 144 per IIP-0118):
weighted_target = sum(target_i * weight_i) / sum(weight_i)
Where weights ramp from 1 (oldest block) to N (newest). The weighted target output is then converted to next-block nBits.
The clamp on each block’s solvetime was asymmetric:
solvetime = block.nTime - parent.nTime
if solvetime > 6*T: solvetime = 6*T // upper clamp — timestamp-attack defence
// no lower clamp on solvetime <= 0
When 23 burst blocks each contribute solvetime ≈ 0, the weighted average target effectively shrinks (smaller solvetimes → harder difficulty needed to maintain T). LWMA output went UP, not down, in response to a chain that was actually under-served with hashrate.
The four IIPs in v1.0.6 / v1.0.6.1
- IIP-0117 — symmetric clamp. Lower bound
T/4matches the upper6×T. Burst zero-solvetimes can’t pin the average to zero anymore. - IIP-0118 — window 60 → 144. A 23-block burst goes from 38% of window to 16%. Single-event noise diluted to where the rest of the window can damp it.
- IIP-0119 — per-retarget damping clamp ±30%. Even if the LWMA window is unusually skewed, the per-block target multiplier is bounded to
[0.7, 1.3]. Smooths oscillation around equilibrium. - IIP-0120 — emergency min-difficulty fallback. If
next_block.nTime - parent.nTime > 20*T(40 minutes at T=120s mainnet), the block may use powLimit difficulty. This is the “unstall the chain regardless of LWMA edge cases” rule. It’s the same idea Bitcoin testnet uses to keep testnet alive when miners turn off.
What this means for miners going forward
Practical implications for solo miners on v1.0.6.1+:
- Don’t burst-mine. Mining 23 blocks in 2 minutes then leaving the network with sub-T-block hashrate is a denial-of-service vector against the LWMA window. v1.0.6.1’s IIP-0117 + 0119 + 0120 stack should self-heal within ~40 minutes, but it’s still bad neighbours.
- If the chain stalls > 40 min, IIP-0120 will eventually fire. Your miner’s
getblocktemplateshould drop to powLimit nBits and your hashrate will land a block in seconds. If it doesn’t, that’s a daemon-version mismatch — checkgetnetworkinfo.subversion. v1.0.6 daemons silently never trigger IIP-0120; v1.0.6.1+ do. - Difficulty oscillation is normal during recovery. After an IIP-0120 powLimit block lands, the next ~60 blocks will see the LWMA window adjusting downward as it absorbs the easier solvetimes. Difficulty will rebound once the window stabilises around T=120s.
- Mining at INTcoin’s current hashrate scale (1 KH/s – 1 MH/s) is unusual — most LWMA papers discuss multi-MH/s chains. The IIP-0117/0118/0119/0120 stack is calibrated for our actual scale, not theoretical equilibrium. Expect the parameters to be revisited if hashrate grows past ~10 MH/s.
What we changed in test discipline
The v1.0.6 regtest case (lwma_emergency_min_diff_after_40_minutes) tested the inner function with nextBlockTime passed explicitly. It was green. It missed the bug because no test exercised the daemon’s BlockAssembler::CreateNewBlock or the validation ContextualCheckBlock path.
Going forward (codified in [feedback_consensus_test_feature_not_function.md] in our internal review notes): every consensus rule that depends on a per-block input must have at least one test that exercises the rule through the public-API path, not just the inner deterministic function.
That’s the lesson. Was painful to learn at h=8614, but it’s the kind of thing that only burns once.
— INT-devs