Difficulty: Igneum dual-lane rule measured against Kaspa, Monero and LWMA; spec 2.3, analysis, simulator, devnet and test-network records
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
f5f8c805d1
commit
e9328c68ab
9 changed files with 6599 additions and 9 deletions
447
docs/analysis/difficulty-2026-10-03.md
Normal file
447
docs/analysis/difficulty-2026-10-03.md
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
# 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.
|
||||
|
|
@ -260,7 +260,7 @@ Record: 3,682 headers of the overnight devnet pulled read-only through the obser
|
|||
Simulator `sim/difficulty/sim.py` (Python, one chain, exponential solve times, 1 BPS): Kaspa sampled DAA, Monero 720, LWMA 60 and 120, the Igneum rule and the brief's literal trigger, on nine synthetic profiles plus the record. Two design findings: Zawy's average-target LWMA estimator is biased while targets ramp (the fast lane stalled at 15x of a 50x step), so every Igneum lane uses work over time (Kaspa's `estimateNetworkHashesPerSecond` estimator); the brief's trigger (short-window rate off target) chatters once the short window is back on target while the long window is still polluted (polluted case 1,876 s against 70 s), so the trigger compares the two lanes. Tuned on the synthetic set only: short window 120, hold 8, prior 16, long lane from 600 blocks of the epoch, trigger 25%, harden 3% per block, ease 10% per block, solvetime cap 20 T.
|
||||
Settled seconds (121-block mean within 10% for 100 blocks), Kaspa / Monero / LWMA60 / LWMA120 / Igneum: x50 step 1,542 / 94 / 105 / 231 / 62; /50 step 12,296 / 6,433 / 578 / 1,074 / 657 (worst gap 179 / 187 / 119 / 65 / 35 s); epoch +-30% steps 1,583 / 456 / 157 / 153 / 144; 10x hopping never / 284 (6 of 12 never) / 264 / 326 / 212; polluted window 2,748 (peak 7.9x) / 124 (peak 15x) / 66 / 110 / 70; genesis 10x too hard never / 287 / 155 / 188 / 322; steady std of rate 0.012 / 0.037 / 0.131 / 0.092 / 0.038; the record's 75x step never (peak 7.6x, 2,340 blocks above 2x) / 136 / 75 / 157 / 79. With +-500 ms timestamp jitter the ordering holds (Igneum x50 169 s, /50 1,047 s, epoch 55 s, polluted 71 s).
|
||||
Implementation: `DifficultyRule { KaspaSampled, IgneumDual }` as a network parameter (IgneumDual on all four networks, `"difficulty_rule": "kaspa-sampled"` in the override file selects Kaspa's), `OverrideParams.genesis_bits` (genesis hash recomputed) for test networks, `SampledDifficultyManager::igneum_difficulty_bits` over a selected-chain walk plus the in-epoch samples of the existing window, pure integer core `igneum_target` (Uint320). `cargo test -p kaspa-consensus --lib difficulty`: 9 pass (hold, steady state, 3% harden, 10% ease, trigger, epoch shrinkage, max target, Kaspa's two level-work tests); `cargo test -p kaspa-consensus-core --lib params`: 4 pass. The miner now takes the genesis from the node (pruning point before the first pruning) so it mines an override-genesis network; before the fix it hashed the compiled devnet genesis as the epoch seed and every block was rejected.
|
||||
TESTNET_BENCH
|
||||
Test network (ports 26800 to 26821, appdir /tmp/igneum-diff-test, override file with `difficulty_rule` and `genesis_bits` 0x1e200000): 3 igneumd nodes, 3 CPU miners of 4 threads (A for 1,200 s; B and C from 359 s to 779 s), 1,133 blocks, 0 rejected, one sink. Delivered hash rate 0.0653 / 0.0988 / 0.0686 MH/s (the join is x1.51, not 3x: shared cores at load 50 to 90). Genesis 4x too hard. Measured first within 10% of 1 block/s (61-block mean): warm-up 132 s, join 214 s (23 s to first touch), leave 85 s; simulator on the same profile, 5 seeds: medians 263 s, 61 s, 231 s; worst gap 7.3 s. Kaspa's rule on the same genesis: no retarget inside 20 minutes in any seed (600-block dead zone). Kaspa's rule live on the same genesis, 10 minutes: 70 blocks, bits unchanged on all 70, 0.08 blocks/s, worst gap 63 s, never within 10% of target.
|
||||
Not done: no DAG in the simulator (red blocks' work is ignored by every lane, as by Kaspa's estimator); Monero and LWMA reproduced from memory (approximate); real-time targeting left out; the chain walk (600 reads per header early in an epoch) must be re-measured before the 4 BPS step.
|
||||
|
||||
## 3 October 2026, igneum-node devnet v2: sustained-mining finality rule v2 on a four-miner test network, and as a follower of the live devnet (consensus-engineer and cryptographer)
|
||||
|
|
|
|||
|
|
@ -35,17 +35,59 @@ What the nonce commits to: the pre-PoW header hash `H` enters the register initi
|
|||
|
||||
## 2.3 Difficulty adjustment (fork points c1, c2)
|
||||
|
||||
Kaspa's sampled DAA (KIP-4) is kept as the retarget: average target of a sampled window, `new_target = avg x measured / expected`, clamped to `MAX_DIFFICULTY_TARGET` = 2^255 - 1, minimum window 150 samples (`difficulty.rs:97 to 198`).
|
||||
Status: Implemented on the `difficulty` branch of `vendor/igneum-node` (`SampledDifficultyManager::igneum_difficulty_bits`, pure core `igneum_target`, constants `kaspa_consensus_core::igneum::difficulty`), measured in `sim/difficulty/` and on a 3-node CPU test network (`docs/analysis/difficulty-2026-10-03.md`). Network parameter `difficulty_rule`: `igneum-dual` on every Igneum network, `kaspa-sampled` selectable through the override file (`{"difficulty_rule": "kaspa-sampled"}`) so the two rules can be compared on the same genesis. This closes O-2.1: the window is not shortened and the cost-equalising rule of section 1.4.2 is no longer load-bearing for difficulty (it remains desirable for the verifier bound).
|
||||
|
||||
| Parameter | Value | Label |
|
||||
|---|---|---|
|
||||
| Window duration | 2,641 DAA s (Kaspa's `DIFFICULTY_WINDOW_DURATION`) | Designed, prototype value, to be fixed at gate 2. The design document says "about 2,600 blocks, roughly 45 minutes" |
|
||||
| Sample interval | 4 s, 661 samples | Kaspa's constants, kept |
|
||||
| Keyed to | DAA score, never wall-clock | Designed |
|
||||
Lineage, credited: the long lane is Kaspa's KIP-4 sampled window (`difficulty.rs`, samples at every 4th block, 661 samples, `estimateNetworkHashesPerSecond`'s blue-work-over-time estimator); the short lane is Zawy's LWMA weighting (linear weights rising to the newest block, solvetimes clamped symmetrically, 20 target times here against Zawy's 6) applied to the same work-over-time estimator instead of LWMA's average target, which is biased while targets ramp (measured: the fast lane stalled at 15x of a 50x step before the change, `docs/analysis/difficulty-2026-10-03.md`).
|
||||
|
||||
What fixes the window: the hash rate steps at every epoch because programs differ in cost (35 to 48 Mhash/s across seeds on the M5 Max, `docs/bench-log.md` first-run entry; 228 vs 185 Mhash/s for 104 vs 128 loads on the RTX 5090). With a 44-minute window a 30% step means roughly half an epoch at the wrong block rate (fork map c1). Two remedies, decided at gate 2 by a simpa run (fork map c2): shorten the window (candidate 1,800 s), or equalise cost per program through the exact-load-count rule of section 1.4.2, which this specification prefers because it removes the step rather than tracking it.
|
||||
### The rule
|
||||
|
||||
The 30-day vote-weight window of section 3 is denominated in past-median time since 3 October 2026 (section 3.1 W2, ledger F14), so a retarget lag cannot age it; it is not a retarget change.
|
||||
1. Every lane estimates the hash rate as work over time (blue-work increment over clamped solvetime), never as average target times average solvetime.
|
||||
2. Short lane S: the newest 120 selected-chain blocks of the block's own epoch, linear weights. Reference lane R: for the first 600 blocks of an epoch the epoch window (all its blocks, blended with the parent's implied rate as k : 16); from block 600 of the epoch Kaspa's sampled window restricted to the epoch.
|
||||
3. S takes over when S and R differ by more than 25%; otherwise R rules. The trigger compares the two lanes, not the short rate to target (the literal form chatters once the short window is back on target while the long window is still polluted: measured 1,910 s against 72 s to settle on the polluted-window case).
|
||||
4. The output may make the target fall (difficulty rise) by at most 3% per block and rise (difficulty fall) by at most 10% per block, relative to the selected parent's target, then is capped at `MAX_DIFFICULTY_TARGET`.
|
||||
5. The parent's target is held for the first 8 blocks of every epoch, including epoch 0: the warm-up starts at block 8, not 600.
|
||||
|
||||
| Parameter | Value | Constant | Label |
|
||||
|---|---|---|---|
|
||||
| Epoch | 3,600 DAA s, the block's own DAA score (section 1.12) | `POW_EPOCH_BLOCKS` | Designed |
|
||||
| Short window | 120 chain blocks | `SHORT_WINDOW` | Measured: 60 is noisier (steady std 0.069 vs 0.039), 180 slower on hops (356 s vs 276 s) |
|
||||
| Hold at epoch start | 8 blocks | `EPOCH_HOLD` | Designed |
|
||||
| Epoch-lane prior weight | 16 blocks | `PRIOR_BLOCKS` | Measured: 8, 16, 32 indistinguishable |
|
||||
| Long lane from | 600 blocks of the epoch (150 samples) | `LONG_MIN` | Kaspa's minimum window, kept |
|
||||
| Long window | 661 samples at 4-block intervals (2,644 blocks), restricted to the epoch | Kaspa's constants, kept | Designed |
|
||||
| Trigger | 25% disagreement between S and R | `TRIGGER_PERCENT` | Measured: 15% engages on noise (steady std 0.077), 35% slower on hops |
|
||||
| Harden clamp | 3% per block | `HARDEN_PERCENT` | Measured: 2% slows the 50x step-up to 84 s from 62 s, 5% buys nothing on the up step |
|
||||
| Ease clamp | 10% per block | `EASE_PERCENT` | Measured: 3% leaves the 50x step-down at 1,786 s and a 211 s worst gap, 10% gives 1,164 s and 65 s |
|
||||
| Solvetime cap | 20 target times, both signs | `CAP_BLOCKS` | Measured: Zawy's 6 T leaves the 50x step-down at 1,164 s, 20 T gives 657 s and a 35 s worst gap; 132 s (the future tolerance) gains nothing more and lets an idle gap over-ease (16x peak against 8.5x on the devnet case) |
|
||||
| Timestamp rules | 132 s future tolerance in isolation, strictly above the sampled past median in context | Kaspa's, unchanged | Designed |
|
||||
|
||||
What the clamps and caps bound. A forged timestamp can move one capped solvetime by at most 20 s in either direction and the next honest block's solvetime cancels it under the symmetric cap (a run of L forged blocks nets L - 1 s of real time), so a minority cannot bias a lane by more than a few percent; the per-block clamps bound how fast any estimator error, honest or not, reaches the target. The 3% harden clamp means a 50x step up is corrected in about 130 blocks (a few seconds of wall time at 50x), the 10% ease clamp means a 50x step down costs about 40 blocks of shrinking gaps (11 minutes measured, against 3.4 hours for Kaspa's rule).
|
||||
|
||||
### Measured comparison
|
||||
|
||||
Simulator (`sim/difficulty/sim.py`, seed 7, honest timestamps; the full tables with overshoot, gap and noise columns are in the analysis document). "Settled" is the first time the 121-block mean rate stays within 10% of target for 100 blocks.
|
||||
|
||||
| Case | Kaspa sampled DAA | Monero 720 | LWMA 60 | LWMA 120 | Igneum |
|
||||
|---|---|---|---|---|---|
|
||||
| Hash rate x50, settled s | 1,542 | 94 | 105 | 231 | 62 |
|
||||
| Hash rate /50, settled s | 12,296 | 6,433 | 578 | 1,074 | 657 |
|
||||
| Hash rate /50, worst gap s | 179 | 187 | 119 | 65 | 35 |
|
||||
| Epoch step +-30%, settled s (mean of 6) | 1,583 | 456 | 157 | 153 | 144 |
|
||||
| Epoch step +-30%, first within 10% s | 1,583 | 456 | 91 | 137 | 87 |
|
||||
| 3x pool hopping every 15 min, settled s | never | 337 (6 of 12 never) | 167 | 184 | 190 |
|
||||
| 10x pool hopping every 15 min, settled s | never | 284 (6 of 12 never) | 264 | 326 | 212 |
|
||||
| Polluted window (devnet case), settled s | 2,748 (peak 7.9x) | 124 (peak 15x) | 66 | 110 | 70 |
|
||||
| Genesis 10x too hard, settled s | never | 287 | 155 | 188 | 322 |
|
||||
| Genesis 10x too easy, settled s | 1,555 | 95 | 58 | 88 | 59 |
|
||||
| Steady std of block rate, constant hash rate | 0.012 | 0.037 | 0.131 | 0.092 | 0.038 |
|
||||
| Steady std of block rate, 10%/h random walk | 0.049 | 0.049 | 0.132 | 0.093 | 0.062 |
|
||||
| Devnet record (75x step on arrival of the PC), settled s | never (peak 7.6x, 2,340 blocks above 2x) | 136 | 75 | 157 | 79 |
|
||||
|
||||
Test network (3 igneumd nodes, CPU miners, 20 minutes, `docs/analysis/difficulty-2026-10-03.md`): genesis 4x too hard, CPU miners A then A+B+C (x1.51) then A (/1.45) on a loaded machine; measured first-within-10% 132 s, 214 s, 85 s against simulator medians of 263 s, 61 s and 231 s on the same profile; 1,133 blocks, 0 rejected; Kaspa's rule on the same genesis would not have retargeted within the run (600-block dead zone at 0.25 blocks/s). Kaspa's rule live on the same genesis, 10 minutes: 70 blocks, bits unchanged on all 70, 0.08 blocks/s, worst gap 63 s, never within 10% of target.
|
||||
|
||||
### What this section does not do
|
||||
|
||||
Blocks off the selected chain count only through blue work (red blocks carry work the estimator ignores, as Kaspa's estimator does). The chain walk costs up to 600 compact-header reads per block for the first 600 blocks of an epoch and 120 afterwards; re-measure before the 4 BPS step, where the sampled window should replace the walk for the epoch lane too. Real-time targeting (a target that depends on the header's own timestamp) was considered for step-downs and left out: it changes template building and header validation, and the 10% ease clamp with the 20 T cap already brings the 50x step-down to 11 minutes. The 30-day vote-weight window of section 3 is denominated in past-median time since 3 October 2026 (section 3.1 W2, ledger F14), so a retarget lag cannot age it; it is not a retarget change.
|
||||
|
||||
## 2.4 Header (fork point d)
|
||||
|
||||
|
|
|
|||
55
sim/difficulty/README.md
Normal file
55
sim/difficulty/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# sim/difficulty
|
||||
|
||||
Difficulty controller simulator for Igneum (3 October 2026). Model, candidates, results and the
|
||||
devnet record behind `docs/analysis/difficulty-2026-10-03.md` and section 2.3 of `docs/spec/02-consensus.md`.
|
||||
|
||||
## Files
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `sim.py` | the simulator: block production from a hash-rate profile through six controllers, metrics as markdown |
|
||||
| `devnet-2026-10-03.csv` | every header of the overnight devnet (observer node wRPC, `ws://127.0.0.1:28640`): hash, DAA score, blue score, timestamp, bits, difficulty (2^255 / target, the node's convention), log2 target, epoch, chain flag, parent count, selected parent |
|
||||
| `results.md` | raw simulator output: synthetic set, timestamp-noise set, tuning sweeps, record replay, test-network comparison |
|
||||
| `testnet-igneum-2026-10-03.csv`, `testnet-kaspa-2026-10-03.csv` | every header of the two 3-node CPU test networks (Igneum rule, 20 min; Kaspa rule, 10 min), same columns |
|
||||
|
||||
## Model
|
||||
|
||||
One selected chain, DAA score = height, target 1 block per second, solve times exponential with
|
||||
mean (expected hashes per block) / (hash rate). A hash-rate profile is piecewise constant in time
|
||||
(miners joining and leaving) or keyed to the 3,600-block epoch (program cost steps). Timestamps are
|
||||
the true times in milliseconds; `--ts-noise N` adds uniform +-N ms miner clock jitter.
|
||||
|
||||
Metrics per step: first time the 121-block centred mean of the expected block rate is within 10% of
|
||||
target; "settled" = the first time it then stays within 10% for 100 blocks; overshoot past target on
|
||||
the far side; blocks spent above 2x and below 0.5x target rate; worst inter-block gap. Steady state:
|
||||
standard deviation of the expected rate ratio over a steady hour, blocks-per-minute coefficient of
|
||||
variation (Poisson alone gives 0.129 at 60 blocks per minute).
|
||||
|
||||
## Controllers
|
||||
|
||||
| Name | Rule |
|
||||
|---|---|
|
||||
| `kaspa` | KIP-4 sampled DAA exactly as the fork runs it: samples at every 4th block, 661 samples, average target of the window without its earliest block, times measured over expected, genesis bits held until 150 samples (600 blocks) |
|
||||
| `monero` | Monero `next_difficulty`: 720 blocks, timestamps sorted, 60 cut from each end, lag 15 |
|
||||
| `lwma60`, `lwma120` | Zawy LWMA-1 (2018 reference), linear weights, solvetimes in [-132 s, 6 T] |
|
||||
| `igneum` | the Igneum rule of spec 2.3 (two lanes, work-over-time estimators, epoch windows, 3% harden and 10% ease clamps) |
|
||||
| `igneum-literal` | the same with the brief's literal trigger (short-window rate off target by 25%), kept to show the chatter |
|
||||
|
||||
## Runs
|
||||
|
||||
python3 sim.py synthetic set, every controller
|
||||
python3 sim.py --ts-noise 500 the same with +-500 ms timestamp jitter
|
||||
python3 sim.py --tune parameter sweep for the Igneum rule
|
||||
python3 sim.py --record devnet-2026-10-03.csv --events 2026-10-03T19:28:05+00:00,2026-10-03T19:42:40+00:00
|
||||
exact replay check of Kaspa's rule on the record,
|
||||
then the record as a hash-rate profile through every controller
|
||||
python3 sim.py --dump up50:igneum per-block trajectory (t ms, height, D, hash rate, rate ratio)
|
||||
|
||||
Requirements: Python 3 only (numpy not needed). The full synthetic set takes about two minutes.
|
||||
|
||||
## Limits
|
||||
|
||||
No DAG (one chain, no parallel blocks, no reds), no network latency, no miner template refresh
|
||||
delay, honest timestamps unless `--ts-noise`. The exact-replay check of Kaspa's rule matches the
|
||||
record bit for bit while the devnet was a chain and diverges once it widened past one block per
|
||||
second, because a chain-only replay cannot see the merged blocks that the sampled window includes.
|
||||
3683
sim/difficulty/devnet-2026-10-03.csv
Normal file
3683
sim/difficulty/devnet-2026-10-03.csv
Normal file
File diff suppressed because it is too large
Load diff
362
sim/difficulty/results.md
Normal file
362
sim/difficulty/results.md
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
# sim/difficulty results, 3 October 2026
|
||||
|
||||
Raw simulator output behind `docs/analysis/difficulty-2026-10-03.md`. Final parameters: short window 120,
|
||||
hold 8, prior 16, long lane from 600, trigger 25%, harden 3%, ease 10%, cap 20 T.
|
||||
|
||||
## Synthetic set, honest timestamps
|
||||
|
||||
python3 sim.py
|
||||
|
||||
### 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 |
|
||||
|
||||
## Synthetic set, +-500 ms timestamp jitter
|
||||
|
||||
python3 sim.py --ts-noise 500 --profiles up50,down50,epoch30,hop10,polluted
|
||||
|
||||
### 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 | 1535.1 | 1535.1 | 4997 | 0.050 | 0.010 | 0.125 | 8.0 | 3876 | 0 | 21640 | none |
|
||||
| monero | 113.1 | 186.1 | 807 | 0.039 | 0.034 | 0.127 | 8.8 | 656 | 0 | 18496 | none |
|
||||
| lwma60 | 158.7 | 158.7 | 238 | 0.142 | 0.125 | 0.087 | 9.6 | 83 | 0 | 17939 | none |
|
||||
| lwma120 | 139.8 | 139.8 | 305 | 0.115 | 0.085 | 0.104 | 9.3 | 171 | 0 | 18047 | none |
|
||||
| igneum | 168.5 | 168.5 | 288 | 0.024 | 0.046 | 0.133 | 8.6 | 131 | 0 | 18082 | 435 |
|
||||
| igneum-literal | 115.8 | 115.8 | 253 | 0.026 | 0.039 | 0.114 | 8.6 | 143 | 0 | 18145 | 338 |
|
||||
|
||||
### 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 | 12523.3 | 12523.3 | 1531 | 5.826 | 0.010 | 0.125 | 138.0 | 1736 | 1224 | 16001 | none |
|
||||
| monero | 6425.5 | 6425.5 | 686 | 0.061 | 0.034 | 0.127 | 191.9 | 0 | 555 | 19343 | none |
|
||||
| lwma60 | 712.2 | 846.7 | 232 | 0.168 | 0.125 | 0.087 | 77.8 | 0 | 51 | 24398 | none |
|
||||
| lwma120 | 965.9 | 965.9 | 156 | 0.140 | 0.085 | 0.104 | 80.7 | 0 | 93 | 24224 | none |
|
||||
| igneum | 895.5 | 1046.6 | 289 | 0.135 | 0.046 | 0.133 | 49.4 | 0 | 90 | 24438 | 593 |
|
||||
| igneum-literal | 805.8 | 933.2 | 254 | 0.142 | 0.039 | 0.114 | 114.6 | 0 | 91 | 24579 | 307 |
|
||||
|
||||
### 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 | 1534.5 | 1534.5 | 1859.4 | 0 | 0.042 | 0.010 | 0.120 | 10.4 | 0 | 0 | 29420 | none |
|
||||
| monero | 440.2 | 440.2 | 559.7 | 0 | 0.089 | 0.038 | 0.124 | 10.0 | 0 | 0 | 28818 | none |
|
||||
| lwma60 | 63.2 | 63.2 | 131.8 | 0 | 0.256 | 0.119 | 0.087 | 11.9 | 0 | 0 | 28592 | none |
|
||||
| lwma120 | 84.3 | 123.6 | 259.8 | 0 | 0.223 | 0.079 | 0.097 | 10.4 | 0 | 0 | 28657 | none |
|
||||
| igneum | 55.2 | 55.2 | 172.1 | 0 | 0.200 | 0.036 | 0.130 | 9.9 | 0 | 0 | 28888 | 619 |
|
||||
| igneum-literal | 51.4 | 51.4 | 172.1 | 0 | 0.108 | 0.036 | 0.106 | 10.1 | 0 | 0 | 28953 | 276 |
|
||||
|
||||
### 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.010 | 0.125 | 34.3 | 5786 | 1007 | 23012 | none |
|
||||
| monero | 276.9 | 276.9 | 305.1 | 6 | 0.083 | 0.034 | 0.127 | 76.5 | 3569 | 557 | 19172 | none |
|
||||
| lwma60 | 178.1 | 209.9 | 352.7 | 0 | 0.201 | 0.125 | 0.087 | 42.2 | 281 | 192 | 20902 | none |
|
||||
| lwma120 | 307.9 | 317.6 | 729.2 | 0 | 0.115 | 0.085 | 0.104 | 38.8 | 600 | 412 | 20529 | none |
|
||||
| igneum | 205.1 | 228.0 | 491.7 | 0 | 0.130 | 0.046 | 0.133 | 31.9 | 430 | 320 | 20770 | 8333 |
|
||||
| igneum-literal | 473.2 | 382.0 | 647.2 | 9 | 0.086 | 0.039 | 0.114 | 28.1 | 714 | 759 | 20753 | 5370 |
|
||||
|
||||
### 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 | 721.0 | 2722.4 | 5176 | 7.512 | 0.017 | 0.117 | 10.0 | 2384 | 0 | 18668 | none | 8.5 | 0.6 |
|
||||
| monero | 126.9 | 211.0 | 826 | 0.077 | 0.032 | 0.133 | 9.7 | 675 | 0 | 17706 | none | 14.9 | 0.9 |
|
||||
| lwma60 | 89.4 | 89.4 | 131 | 0.168 | 0.128 | 0.088 | 11.9 | 35 | 0 | 17088 | none | 5.7 | 0.6 |
|
||||
| lwma120 | 173.7 | 173.7 | 259 | 0.138 | 0.086 | 0.092 | 10.4 | 77 | 0 | 17152 | none | 5.9 | 0.7 |
|
||||
| igneum | 71.3 | 71.3 | 142 | 0.152 | 0.042 | 0.120 | 10.6 | 81 | 0 | 17139 | 2823 | 8.5 | 0.7 |
|
||||
| igneum-literal | 715.3 | 2041.7 | 2720 | 0.110 | 0.036 | 0.120 | 11.1 | 633 | 0 | 17775 | 2018 | 8.5 | 0.7 |
|
||||
|
||||
## The devnet record
|
||||
|
||||
python3 sim.py --profiles none --record devnet-2026-10-03.csv --events 2026-10-03T19:28:05+00:00,2026-10-03T19:42:40+00:00
|
||||
|
||||
### Exact replay of Kaspa's rule on the record
|
||||
|
||||
Blocks 3682, chain blocks 1656, retargets checked 1056, exact before the first mismatch 0, mismatches 1056, first at daa 845: have 0x1e00a097 want 0x1e009b9a
|
||||
(A chain-only replay cannot see the merged blocks that Kaspa's sampled window includes once the DAG widens.)
|
||||
|
||||
Real profile segments (hash rate estimated as expected hashes over wall time):
|
||||
|
||||
| from s | to s | blocks/s at genesis difficulty | MH/s |
|
||||
|---|---|---|---|
|
||||
| 0 | 1215 | 0.114 | 30.7 |
|
||||
| 1215 | 2090 | 0.006 | 1.5 |
|
||||
| 2090 | 4064 | 0.433 | 116.2 |
|
||||
|
||||
### real: the devnet record replayed as a hash-rate profile
|
||||
|
||||
| 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 | blocks within 10% | step factor |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| kaspa | 261.9 | none | none | 6.244 | 1.599 | 0.567 | 7.8 | 2340 | 0 | 4776 | none | 0 | 75.7 |
|
||||
| monero | 135.9 | 135.9 | 729 | 0.092 | 0.027 | 0.119 | 6.8 | 672 | 0 | 3729 | none | 2822 | 75.7 |
|
||||
| lwma60 | 74.6 | 74.6 | 157 | 0.183 | 0.109 | 0.067 | 7.4 | 87 | 0 | 3890 | none | 2175 | 75.7 |
|
||||
| lwma120 | 156.5 | 156.5 | 330 | 0.147 | 0.084 | 0.086 | 7.1 | 174 | 0 | 3691 | none | 2390 | 75.7 |
|
||||
| igneum | 78.7 | 78.7 | 192 | 0.180 | 0.087 | 0.105 | 7.3 | 132 | 0 | 3740 | 2484 | 2608 | 75.7 |
|
||||
| igneum-literal | 482.4 | 1516.2 | 2112 | 0.008 | 0.456 | 0.186 | 7.4 | 515 | 0 | 4119 | 1959 | 2170 | 75.7 |
|
||||
|
||||
## Tuning sweeps
|
||||
|
||||
First sweep (symmetric 3% clamp, cap 6 T baseline):
|
||||
|
||||
### 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 (harden 3%, ease 10%, cap 6 T baseline; the cap row fixed the final value at 20 T):
|
||||
|
||||
### 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 |
|
||||
|
||||
## Test network
|
||||
|
||||
Igneum rule, 3 nodes, 3 CPU miners, 20 minutes (`testnet-igneum-2026-10-03.csv`), analysed with the same 61-block mean as the simulator:
|
||||
|
||||
```
|
||||
block 1 at 2026-10-03T21:38:32.221000 schedule start 21:38:31 = 1 s before block 1
|
||||
miner starts (s from block 1): [('A', -1.221), ('B', 358.779), ('C', 358.779), ('B+C stop', 778.8)]
|
||||
| from s | blocks | blocks/s | mean difficulty | MH/s (work over 30 s) |
|
||||
|---|---|---|---|---|
|
||||
| 0 | 5 | 0.17 | 262,143 | 0.087 |
|
||||
| 30 | 3 | 0.10 | 262,143 | 0.052 |
|
||||
| 60 | 6 | 0.20 | 190,283 | 0.076 |
|
||||
| 90 | 22 | 0.73 | 62,248 | 0.091 |
|
||||
| 120 | 16 | 0.53 | 33,904 | 0.036 |
|
||||
| 150 | 25 | 0.83 | 31,146 | 0.052 |
|
||||
| 180 | 37 | 1.23 | 31,340 | 0.077 |
|
||||
| 210 | 38 | 1.27 | 32,492 | 0.082 |
|
||||
| 240 | 27 | 0.90 | 32,722 | 0.059 |
|
||||
| 270 | 30 | 1.00 | 32,591 | 0.065 |
|
||||
| 300 | 29 | 0.97 | 32,698 | 0.063 |
|
||||
| 330 | 28 | 0.93 | 32,395 | 0.060 |
|
||||
| 360 | 57 | 1.90 | 35,345 | 0.134 |
|
||||
| 390 | 25 | 0.83 | 44,446 | 0.074 |
|
||||
| 420 | 26 | 0.87 | 35,183 | 0.061 |
|
||||
| 450 | 23 | 0.77 | 34,670 | 0.053 |
|
||||
| 480 | 39 | 1.30 | 34,488 | 0.090 |
|
||||
| 510 | 34 | 1.13 | 34,902 | 0.079 |
|
||||
| 540 | 48 | 1.60 | 38,841 | 0.124 |
|
||||
| 570 | 27 | 0.90 | 49,788 | 0.090 |
|
||||
| 600 | 31 | 1.03 | 49,682 | 0.103 |
|
||||
| 630 | 39 | 1.30 | 56,451 | 0.147 |
|
||||
| 660 | 23 | 0.77 | 57,562 | 0.088 |
|
||||
| 690 | 36 | 1.20 | 57,271 | 0.137 |
|
||||
| 720 | 20 | 0.67 | 58,646 | 0.078 |
|
||||
| 750 | 35 | 1.17 | 51,839 | 0.121 |
|
||||
| 780 | 16 | 0.53 | 43,030 | 0.046 |
|
||||
| 810 | 19 | 0.63 | 40,907 | 0.052 |
|
||||
| 840 | 26 | 0.87 | 40,517 | 0.070 |
|
||||
| 870 | 15 | 0.50 | 39,733 | 0.040 |
|
||||
| 900 | 30 | 1.00 | 28,790 | 0.058 |
|
||||
| 930 | 17 | 0.57 | 26,404 | 0.030 |
|
||||
| 960 | 43 | 1.43 | 24,354 | 0.070 |
|
||||
| 990 | 27 | 0.90 | 26,346 | 0.047 |
|
||||
| 1020 | 40 | 1.33 | 29,237 | 0.078 |
|
||||
| 1050 | 42 | 1.40 | 38,348 | 0.107 |
|
||||
| 1080 | 39 | 1.30 | 42,203 | 0.110 |
|
||||
| 1110 | 21 | 0.70 | 41,639 | 0.058 |
|
||||
| 1140 | 39 | 1.30 | 38,832 | 0.101 |
|
||||
| 1170 | 30 | 1.00 | 39,650 | 0.079 |
|
||||
|
||||
segments (hash rate = expected hashes over wall time):
|
||||
0 to 359 s: 0.0653 MH/s
|
||||
359 to 779 s: 0.0988 MH/s
|
||||
779 to 1192 s: 0.0686 MH/s
|
||||
|
||||
recovery after each event (61-block centred mean of expected rate within 10% of 1 block/s):
|
||||
start at 0 s: first within 10% 132 s, settled (60 blocks) 132 s, worst gap 15.4 s, blocks after 1133
|
||||
A at -1 s: first within 10% 134 s, settled (60 blocks) 134 s, worst gap 15.4 s, blocks after 1133
|
||||
B at 359 s: first within 10% 214 s, settled (60 blocks) 214 s, worst gap 7.3 s, blocks after 869
|
||||
C at 359 s: first within 10% 214 s, settled (60 blocks) 214 s, worst gap 7.3 s, blocks after 869
|
||||
B+C stop at 779 s: first within 10% 85 s, settled (60 blocks) none s, worst gap 7.3 s, blocks after 404
|
||||
```
|
||||
|
||||
Kaspa rule, same network and genesis, 10 minutes (`testnet-kaspa-2026-10-03.csv`): 70 blocks, bits 0x1e200000 on every header, 0.08 blocks/s, worst gap 63 s, delivered 0.042 MH/s; no retarget (block 600 was about 2 hours away).
|
||||
794
sim/difficulty/sim.py
Normal file
794
sim/difficulty/sim.py
Normal file
|
|
@ -0,0 +1,794 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Igneum difficulty controller simulator.
|
||||
|
||||
Replays block production from a hash-rate profile through candidate difficulty controllers and
|
||||
reports how each one tracks the target rate. Chain model: one selected chain, DAA score = height,
|
||||
1 block per second target, block times exponential with mean (expected hashes per block) / (hash rate).
|
||||
Timestamps are the true block times in milliseconds (option --ts-noise adds miner clock jitter).
|
||||
|
||||
Controllers
|
||||
kaspa Kaspa's sampled DAA (KIP-4) as the fork runs it: samples every 4th block, 661 samples,
|
||||
average target of the window without its earliest block, times measured over expected,
|
||||
fixed genesis bits for the first 600 blocks (150 samples).
|
||||
monero Monero style: last 720 blocks, timestamps sorted, 60 cut from each end, lag 15.
|
||||
lwma60 Zawy LWMA-1, N = 60, solvetimes clamped to [-FTL, 6T], FTL = 132 s.
|
||||
lwma120 the same with N = 120.
|
||||
igneum the Igneum candidate (two lanes, epoch windows, 3% harden and 10% ease clamps, 20 T solvetime cap, warm-up from block 8).
|
||||
igneum-literal
|
||||
the brief's literal trigger (fast lane engaged while the short-window RATE is more than
|
||||
25% off target) kept for the chatter measurement.
|
||||
|
||||
Profiles
|
||||
real the devnet record (--record CSV) turned into a step profile of estimated hash rate.
|
||||
up50, down50, epoch30, walk10, hop3, hop10, warmup-hard, warmup-easy
|
||||
|
||||
Usage
|
||||
python3 sim.py every synthetic profile, every controller, markdown out
|
||||
python3 sim.py --record devnet-2026-10-03.csv adds the real profile and the exact-bits replay check
|
||||
python3 sim.py --tune the parameter sweep for the Igneum candidate
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
T = 1000 # target time per block, ms
|
||||
EPOCH = 3600 # blocks per program epoch
|
||||
FTL = 132 * 1000 # Kaspa's future timestamp tolerance, ms
|
||||
MAX_TARGET = (1 << 255) - 1
|
||||
ONE = float(1 << 255) # genesis-independent scale: target = ONE / D, D = expected hashes
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- controllers
|
||||
class Controller:
|
||||
name = "base"
|
||||
|
||||
def __init__(self, genesis_target):
|
||||
self.g = genesis_target
|
||||
self.ts = [] # timestamps ms, index = height
|
||||
self.tg = [] # targets (float), index = height
|
||||
|
||||
def push(self, ts, target):
|
||||
self.ts.append(ts)
|
||||
self.tg.append(target)
|
||||
|
||||
def next_target(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def clamp(x, lo, hi):
|
||||
return lo if x < lo else hi if x > hi else x
|
||||
|
||||
|
||||
class Kaspa(Controller):
|
||||
"""KIP-4 sampled DAA as in rusty-kaspa `SampledDifficultyManager::calculate_difficulty_bits`.
|
||||
Sampling: block at height h is a sample when (h_parent_daa + 1) % rate == 0, i.e. h % 4 == 3 on a chain;
|
||||
genesis is never a sample. Window keeps the 661 highest samples. Retarget needs 150 samples."""
|
||||
name = "kaspa"
|
||||
|
||||
def __init__(self, g, window=661, rate=4, min_samples=150):
|
||||
super().__init__(g)
|
||||
self.window = window
|
||||
self.rate = rate
|
||||
self.min_samples = min_samples
|
||||
self.samples = deque() # (ts, target) of sampled blocks, oldest first
|
||||
|
||||
def push(self, ts, target):
|
||||
h = len(self.ts)
|
||||
super().push(ts, target)
|
||||
if h > 0 and h % self.rate == self.rate - 1:
|
||||
self.samples.append((ts, target))
|
||||
if len(self.samples) > self.window:
|
||||
self.samples.popleft()
|
||||
|
||||
def next_target(self):
|
||||
n = len(self.samples)
|
||||
if n < self.min_samples:
|
||||
return self.tg[-1] if self.tg else self.g
|
||||
min_i = min(range(n), key=lambda i: self.samples[i][0])
|
||||
max_i = max(range(n), key=lambda i: self.samples[i][0])
|
||||
min_ts = self.samples[min_i][0]
|
||||
max_ts = self.samples[max_i][0]
|
||||
tsum = sum(t for i, (_, t) in enumerate(self.samples) if i != min_i)
|
||||
avg = tsum / (n - 1)
|
||||
measured = max(max_ts - min_ts, 1)
|
||||
expected = T * self.rate * (n - 1)
|
||||
return min(avg * measured / expected, ONE)
|
||||
|
||||
|
||||
class Monero(Controller):
|
||||
"""Monero next_difficulty: N 720, cut 60 each side on sorted timestamps, lag 15.
|
||||
Difficulty units, converted to target at the end."""
|
||||
name = "monero"
|
||||
|
||||
def __init__(self, g, n=720, cut=60, lag=15):
|
||||
super().__init__(g)
|
||||
self.n, self.cut, self.lag = n, cut, lag
|
||||
|
||||
def next_target(self):
|
||||
h = len(self.ts)
|
||||
end = h - self.lag
|
||||
if end < 2:
|
||||
return self.g
|
||||
start = max(0, end - self.n)
|
||||
ts = sorted(self.ts[start:end])
|
||||
length = len(ts)
|
||||
if length <= 2 * self.cut + 2:
|
||||
cb, ce = 0, length
|
||||
else:
|
||||
cb = (length - (self.n - 2 * self.cut) + 1) // 2 if length >= self.n else self.cut
|
||||
cb = clamp(cb, 0, self.cut)
|
||||
ce = length - cb
|
||||
if ce - cb < 2:
|
||||
cb, ce = 0, length
|
||||
span = max(ts[ce - 1] - ts[cb], 1)
|
||||
work = sum(ONE / t for t in self.tg[start + cb:start + ce])
|
||||
d = work * T / span
|
||||
return min(ONE / d, ONE)
|
||||
|
||||
|
||||
class Lwma(Controller):
|
||||
"""Zawy LWMA-1 (2018 reference): linear weights, solvetime in [-FTL, 6T], average target over N.
|
||||
Grows from 4 blocks instead of waiting for N."""
|
||||
name = "lwma"
|
||||
|
||||
def __init__(self, g, n=120, lo=-FTL, hi=6 * T, min_blocks=4):
|
||||
super().__init__(g)
|
||||
self.n, self.lo, self.hi, self.min_blocks = n, lo, hi, min_blocks
|
||||
self.name = "lwma%d" % n
|
||||
|
||||
def next_target(self):
|
||||
h = len(self.ts)
|
||||
if h < self.min_blocks + 1:
|
||||
return self.g
|
||||
n = min(self.n, h - 1)
|
||||
ts = self.ts
|
||||
wsum = 0.0
|
||||
tsum = 0.0
|
||||
for i in range(1, n + 1):
|
||||
j = h - n - 1 + i
|
||||
st = clamp(ts[j] - ts[j - 1], self.lo, self.hi)
|
||||
wsum += st * i
|
||||
tsum += self.tg[j]
|
||||
wsum = max(wsum, n * n * T / 20)
|
||||
avg = tsum / n
|
||||
return min(avg * wsum / (T * n * (n + 1) / 2), ONE)
|
||||
|
||||
|
||||
class Igneum(Controller):
|
||||
"""Igneum dual-lane controller (docs/spec/02-consensus.md section 2.3).
|
||||
|
||||
Every lane estimates the hash rate as work over time (what Kaspa's estimateNetworkHashesPerSecond does),
|
||||
never as average target times average solvetime, which is biased while targets ramp.
|
||||
k = blocks of the current epoch already in the past (epoch = height // 3600).
|
||||
S (short): linear-weighted work over capped solvetimes of the last min(NS, k) chain blocks.
|
||||
E (epoch): work over capped time of the whole epoch so far, shrunk toward the parent's implied rate by
|
||||
k / (k + K0); used as the reference while k < LONG_MIN.
|
||||
L (long): blue work between the earliest and latest sample of Kaspa's sampled window restricted to the
|
||||
epoch, over their time span; the reference once k >= LONG_MIN.
|
||||
Candidate = S when S and the reference disagree by more than TRIG, else the reference. The output may
|
||||
fall (harder) by at most CLAMP and rise (easier) by at most EASE per block. The parent's target is held
|
||||
for the first K_MIN blocks of an epoch. Genesis bits only for blocks 1 to K_MIN."""
|
||||
name = "igneum"
|
||||
|
||||
def __init__(self, g, ns=120, k_min=8, k0=16, long_min=600, trig=0.25, clamp_pct=0.03, ease_pct=0.10, cap=20 * T,
|
||||
window=661, rate=4, literal=False, epoch_aware=True, kaspa_formula=False):
|
||||
super().__init__(g)
|
||||
self.ns, self.k_min, self.k0, self.long_min = ns, k_min, k0, long_min
|
||||
self.trig, self.clamp_pct, self.cap = trig, clamp_pct, cap
|
||||
self.ease_pct = clamp_pct if ease_pct is None else ease_pct
|
||||
self.window, self.rate = window, rate
|
||||
self.literal = literal
|
||||
self.epoch_aware = epoch_aware
|
||||
self.kaspa_formula = kaspa_formula
|
||||
self.samples = deque() # (height, ts, blue_work)
|
||||
self.pref_d = [0.0] # prefix sums of D (work), index h+1 = work through block h = blue work
|
||||
self.pref_t = [0.0] # prefix sums of targets (only for the kaspa_formula variant)
|
||||
self.pref_st = [0.0] # prefix sums of capped solvetimes
|
||||
self.engaged = 0
|
||||
|
||||
def push(self, ts, target):
|
||||
h = len(self.ts)
|
||||
st = clamp(ts - self.ts[-1], -self.cap, self.cap) if h > 0 else 0
|
||||
super().push(ts, target)
|
||||
self.pref_d.append(self.pref_d[-1] + ONE / target)
|
||||
self.pref_t.append(self.pref_t[-1] + target)
|
||||
self.pref_st.append(self.pref_st[-1] + st)
|
||||
if h > 0 and h % self.rate == self.rate - 1:
|
||||
self.samples.append((h, ts, self.pref_d[-1]))
|
||||
if len(self.samples) > self.window:
|
||||
self.samples.popleft()
|
||||
|
||||
def rate_s(self, lo, hi):
|
||||
"""Linear-weighted hash rate over chain steps lo+1..hi-1 (work of block j over solvetime of block j)."""
|
||||
n = hi - lo - 1
|
||||
if n < 1:
|
||||
return None
|
||||
wsum = 0.0
|
||||
dsum = 0.0
|
||||
ts = self.ts
|
||||
for i in range(1, n + 1):
|
||||
j = lo + i
|
||||
wsum += clamp(ts[j] - ts[j - 1], -self.cap, self.cap) * i
|
||||
dsum += (self.pref_d[j + 1] - self.pref_d[j]) * i
|
||||
wsum = max(wsum, n * n * T / 20)
|
||||
return dsum / wsum
|
||||
|
||||
def rate_e(self, lo, hi, k):
|
||||
n = hi - lo - 1
|
||||
if n < 1:
|
||||
return None
|
||||
work = self.pref_d[hi] - self.pref_d[lo + 1]
|
||||
measured = max(self.pref_st[hi] - self.pref_st[lo + 1], 1)
|
||||
r = work / measured
|
||||
prior = (self.pref_d[hi] - self.pref_d[hi - 1]) / T # the parent's implied rate
|
||||
return (k * r + self.k0 * prior) / (k + self.k0)
|
||||
|
||||
def rate_l(self, lo):
|
||||
samp = [s for s in self.samples if s[0] >= lo]
|
||||
n = len(samp)
|
||||
if n < self.long_min // self.rate:
|
||||
return None
|
||||
mn = min(samp, key=lambda s: s[1])
|
||||
mx = max(samp, key=lambda s: s[1])
|
||||
measured = max(mx[1] - mn[1], 1)
|
||||
if self.kaspa_formula:
|
||||
tsum = sum(self.tg[s[0]] for s in samp if s is not mn)
|
||||
return 1.0 / ((tsum / (n - 1)) * measured / (T * self.rate * (n - 1))) / ONE * ONE * (1.0 / T) * T if False else None
|
||||
return (mx[2] - mn[2]) / measured
|
||||
|
||||
def next_target(self):
|
||||
h = len(self.ts)
|
||||
if h == 0:
|
||||
return self.g
|
||||
parent = self.tg[-1]
|
||||
epoch_start = (h // EPOCH) * EPOCH if self.epoch_aware else 0
|
||||
k = h - epoch_start
|
||||
if k < self.k_min:
|
||||
cand = parent
|
||||
else:
|
||||
s_lo = max(epoch_start, h - self.ns)
|
||||
r_s = self.rate_s(s_lo, h)
|
||||
r_ref = None
|
||||
if k >= self.long_min:
|
||||
r_ref = self.rate_l(max(epoch_start, h - self.window * self.rate))
|
||||
if r_ref is None:
|
||||
r_ref = self.rate_e(epoch_start, h, k)
|
||||
if self.literal:
|
||||
n = h - s_lo
|
||||
measured = max(self.pref_st[h] - self.pref_st[s_lo + 1], 1)
|
||||
use_s = abs(T * (n - 1) / measured - 1) > self.trig
|
||||
else:
|
||||
use_s = abs(r_s / r_ref - 1) > self.trig
|
||||
r = r_s if use_s else r_ref
|
||||
self.engaged += 1 if use_s else 0
|
||||
cand = ONE / (r * T)
|
||||
out = clamp(cand, parent / (1 + self.clamp_pct), parent * (1 + self.ease_pct))
|
||||
return min(out, ONE)
|
||||
|
||||
|
||||
def make_controller(name, g, **kw):
|
||||
if name == "kaspa":
|
||||
return Kaspa(g)
|
||||
if name == "monero":
|
||||
return Monero(g)
|
||||
if name == "lwma60":
|
||||
return Lwma(g, n=60)
|
||||
if name == "lwma120":
|
||||
return Lwma(g, n=120)
|
||||
if name == "igneum":
|
||||
return Igneum(g, **kw)
|
||||
if name == "igneum-literal":
|
||||
return Igneum(g, literal=True, **kw)
|
||||
if name == "igneum-noepoch":
|
||||
return Igneum(g, epoch_aware=False, **kw)
|
||||
raise ValueError(name)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- profiles
|
||||
class Profile:
|
||||
"""Hash rate in hashes per ms as a function of (time ms, height). `steps` lists (t_ms, factor) events
|
||||
for the recovery metrics; `epoch_steps` are height-keyed factors applied at epoch boundaries."""
|
||||
|
||||
def __init__(self, name, base, duration_ms, genesis_d=None, seed=7):
|
||||
self.name = name
|
||||
self.base = base
|
||||
self.duration = duration_ms
|
||||
self.time_steps = [] # (t_ms, factor)
|
||||
self.epoch_steps = {} # height -> factor
|
||||
self.walk = None # (sigma per minute)
|
||||
self.genesis_d = genesis_d if genesis_d is not None else base * T
|
||||
self.rng = random.Random(seed)
|
||||
self._walk_cache = {}
|
||||
|
||||
def rate(self, t, h):
|
||||
f = 1.0
|
||||
for ts, fac in self.time_steps:
|
||||
if t >= ts:
|
||||
f *= fac
|
||||
for hh, fac in self.epoch_steps.items():
|
||||
if h >= hh:
|
||||
f *= fac
|
||||
if self.walk:
|
||||
m = int(t // 60000)
|
||||
if m not in self._walk_cache:
|
||||
prev = self._walk_cache.get(m - 1, 0.0) if m > 0 else 0.0
|
||||
self._walk_cache[m] = prev + self.rng.gauss(0, self.walk)
|
||||
f *= math.exp(self._walk_cache[m])
|
||||
return self.base * f
|
||||
|
||||
def events(self):
|
||||
"""Step events as (t_ms or None, height or None, factor)."""
|
||||
out = [(t, None, f) for t, f in self.time_steps]
|
||||
out += [(None, h, f) for h, f in self.epoch_steps.items()]
|
||||
return out
|
||||
|
||||
|
||||
SCALE = float(1 << 28) # synthetic difficulty unit: 2^28 hashes per block, the devnet genesis
|
||||
H1 = SCALE / T # hash rate that gives one block per second at D = SCALE
|
||||
|
||||
|
||||
def synthetic_profiles(seed):
|
||||
P = []
|
||||
hour = 3600 * 1000
|
||||
p = Profile("up50", H1, 5 * hour, seed=seed); p.time_steps = [(3 * hour, 50.0)]; P.append(p)
|
||||
p = Profile("down50", 50 * H1, 7 * hour, seed=seed); p.time_steps = [(3 * hour, 1 / 50.0)]; P.append(p)
|
||||
p = Profile("epoch30", H1, 8 * hour, seed=seed)
|
||||
rng = random.Random(seed)
|
||||
fac = 1.0
|
||||
for e in range(2, 8):
|
||||
s = rng.choice([1.3, 1 / 1.3])
|
||||
p.epoch_steps[e * EPOCH] = s
|
||||
P.append(p)
|
||||
p = Profile("walk10", H1, 6 * hour, seed=seed); p.walk = 0.10 / math.sqrt(60); P.append(p)
|
||||
p = Profile("hop3", H1, 6 * hour, seed=seed)
|
||||
p.time_steps = [(3 * hour + i * 15 * 60000, 3.0 if i % 2 == 0 else 1 / 3.0) for i in range(12)]
|
||||
P.append(p)
|
||||
p = Profile("hop10", H1, 6 * hour, seed=seed)
|
||||
p.time_steps = [(3 * hour + i * 15 * 60000, 10.0 if i % 2 == 0 else 1 / 10.0) for i in range(12)]
|
||||
P.append(p)
|
||||
# the devnet of 3 Oct 2026: genesis sized for 0.6 blocks/s of GPU, a 0.11 blocks/s Metal worker for 21 min,
|
||||
# 13 min of nothing, then the PC; Kaspa's first retarget at block 600 sees the whole slow history
|
||||
p = Profile("polluted", 0.6 * H1, 5 * hour, genesis_d=1.0 * SCALE, seed=seed)
|
||||
p.time_steps = [(0, 0.11 / 0.6), (21 * 60000, 1e-9), (34 * 60000, 0.6 / 0.11 / 1e-9)]
|
||||
P.append(p)
|
||||
p = Profile("warmup-hard", H1, 2 * hour, genesis_d=10.0 * SCALE, seed=seed); p.time_steps = [(0, 1.0)]; P.append(p)
|
||||
p = Profile("warmup-easy", H1, 2 * hour, genesis_d=0.1 * SCALE, seed=seed); p.time_steps = [(0, 1.0)]; P.append(p)
|
||||
return P
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- record (real devnet)
|
||||
def load_record(path):
|
||||
"""Returns (chain, all): chain blocks (one per blue score, for the exact replay) and every block
|
||||
(for the hash-rate profile), as (blue_score, daa_score, timestamp_ms, bits)."""
|
||||
rows = list(csv.DictReader(open(path)))
|
||||
rows.sort(key=lambda r: (int(r["blue_score"]), int(r["daa_score"]), int(r["timestamp_ms"])))
|
||||
every = [(int(r["blue_score"]), int(r["daa_score"]), int(r["timestamp_ms"]), int(r["bits"], 16)) for r in rows]
|
||||
chain = []
|
||||
seen = set()
|
||||
for r in rows:
|
||||
if r["is_chain_block"] != "1" and r["blue_score"] != "0":
|
||||
continue
|
||||
bs = int(r["blue_score"])
|
||||
if bs in seen:
|
||||
continue
|
||||
seen.add(bs)
|
||||
chain.append((bs, int(r["daa_score"]), int(r["timestamp_ms"]), int(r["bits"], 16)))
|
||||
return chain, every
|
||||
|
||||
|
||||
def bits_to_target(bits):
|
||||
exp = bits >> 24
|
||||
mant = bits & 0x7FFFFF
|
||||
if exp <= 3:
|
||||
return mant >> (8 * (3 - exp))
|
||||
return mant << (8 * (exp - 3))
|
||||
|
||||
|
||||
def target_to_bits(t):
|
||||
"""Uint256::compact_target_bits: size in bytes, mantissa top 3 bytes, sign-bit carry."""
|
||||
if t == 0:
|
||||
return 0
|
||||
size = (t.bit_length() + 7) // 8
|
||||
if size <= 3:
|
||||
mant = t << (8 * (3 - size))
|
||||
else:
|
||||
mant = t >> (8 * (size - 3))
|
||||
if mant & 0x800000:
|
||||
mant >>= 8
|
||||
size += 1
|
||||
return (size << 24) | mant
|
||||
|
||||
|
||||
def kaspa_bits_exact(record, window=661, rate=4, min_samples=150):
|
||||
"""Replay the record's own timestamps and bits through rusty-kaspa's integer arithmetic; return
|
||||
(checked, mismatches). Sample rule as in `sampled_mergeset_iterator` on a chain."""
|
||||
samples = deque()
|
||||
checked = mism = 0
|
||||
first_bad = None
|
||||
exact_before_first = 0
|
||||
for i, (bs, daa, ts, bits) in enumerate(record):
|
||||
if i > 0:
|
||||
n = len(samples)
|
||||
if n >= min_samples:
|
||||
min_i = min(range(n), key=lambda j: samples[j][0])
|
||||
max_i = max(range(n), key=lambda j: samples[j][0])
|
||||
tsum = sum(t for j, (_, t) in enumerate(samples) if j != min_i)
|
||||
avg = tsum // (n - 1)
|
||||
measured = max(samples[max_i][0] - samples[min_i][0], 1)
|
||||
expected = T * rate * (n - 1)
|
||||
new = min(avg * measured // expected, MAX_TARGET)
|
||||
want = target_to_bits(new)
|
||||
checked += 1
|
||||
if want != bits:
|
||||
mism += 1
|
||||
if first_bad is None:
|
||||
first_bad = (daa, hex(bits), hex(want))
|
||||
elif first_bad is None:
|
||||
exact_before_first += 1
|
||||
parent_daa = record[i - 1][1] if i > 0 else None
|
||||
if i > 0 and (parent_daa + 1) % rate == 0:
|
||||
samples.append((ts, bits_to_target(bits)))
|
||||
if len(samples) > window:
|
||||
samples.popleft()
|
||||
return checked, mism, first_bad, exact_before_first
|
||||
|
||||
|
||||
def record_profile(record, every, events, seed):
|
||||
"""Piecewise-constant hash rate estimated from the record between the given event times.
|
||||
events: sorted list of (t_ms, label). Rate in a segment = sum of expected hashes / duration."""
|
||||
t0 = record[1][2]
|
||||
tend = record[-1][2]
|
||||
bounds = [t0] + [t for t, _ in events if t0 < t < tend] + [tend]
|
||||
segs = []
|
||||
for a, b in zip(bounds, bounds[1:]):
|
||||
hashes = sum(2 * ONE / bits_to_target(bits) for (_, _, ts, bits) in every[1:] if a < ts <= b)
|
||||
dur = max(b - a, 1)
|
||||
segs.append((a, b, hashes / dur))
|
||||
p = Profile("real", segs[0][2], tend - t0, genesis_d=ONE / bits_to_target(record[0][3]), seed=seed)
|
||||
for (a, b, r), (a2, b2, r2) in zip(segs, segs[1:]):
|
||||
p.time_steps.append((a2 - t0, r2 / r if r > 0 else 1.0))
|
||||
p.segments = [(a - t0, b - t0, r) for a, b, r in segs]
|
||||
p.min_rate = 1e-12 * H1
|
||||
return p
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- simulation
|
||||
def simulate(profile, ctrl, rng, ts_noise=0, max_blocks=400000):
|
||||
"""Returns list of (t_ms, height, D, hashrate, r) per block, r = expected block rate / target."""
|
||||
t = 0
|
||||
out = []
|
||||
target = ONE / profile.genesis_d
|
||||
ctrl.push(0, target) # genesis at t = 0
|
||||
h = 1
|
||||
while t < profile.duration and h < max_blocks:
|
||||
target = ctrl.next_target()
|
||||
D = ONE / target
|
||||
work = rng.expovariate(1.0) * D # hashes needed for this block
|
||||
t_start = t
|
||||
tt = t
|
||||
while True:
|
||||
hr = profile.rate(tt, h)
|
||||
nxt = min((st for st, _ in profile.time_steps if st > tt), default=None)
|
||||
if hr > 0 and (nxt is None or work <= hr * (nxt - tt)):
|
||||
tt += work / hr
|
||||
break
|
||||
if nxt is None: # zero hash rate for ever: end the run
|
||||
tt = profile.duration + 1
|
||||
break
|
||||
work -= hr * (nxt - tt)
|
||||
tt = nxt
|
||||
gap = tt - t_start
|
||||
hr = profile.rate(tt, h)
|
||||
gap = max(gap, 1.0)
|
||||
t += gap
|
||||
ts = int(t) + (rng.randint(-ts_noise, ts_noise) if ts_noise else 0)
|
||||
ctrl.push(ts, target)
|
||||
r = hr * T / D
|
||||
out.append((t, h, D, hr, r))
|
||||
h += 1
|
||||
return out
|
||||
|
||||
|
||||
def smoothed(xs, half):
|
||||
"""Centred moving mean over 2*half+1 values (shorter at the ends)."""
|
||||
n = len(xs)
|
||||
pref = [0.0]
|
||||
for x in xs:
|
||||
pref.append(pref[-1] + x)
|
||||
out = []
|
||||
for i in range(n):
|
||||
lo = max(0, i - half)
|
||||
hi = min(n, i + half + 1)
|
||||
out.append((pref[hi] - pref[lo]) / (hi - lo))
|
||||
return out
|
||||
|
||||
|
||||
def metrics(blocks, profile, t_step=None, h_step=None, settle_blocks=100, t_end=None):
|
||||
"""Recovery after one step, overshoot, steady state and gap statistics."""
|
||||
res = {}
|
||||
if t_step is not None or h_step is not None:
|
||||
if t_step is None:
|
||||
t_step = next((b[0] for b in blocks if b[1] >= h_step), None)
|
||||
post = [b for b in blocks if b[0] >= t_step and (t_end is None or b[0] < t_end)] if t_step is not None else []
|
||||
if not post:
|
||||
res["recover_s"] = None
|
||||
res["overshoot"] = None
|
||||
else:
|
||||
rec = None
|
||||
sm = smoothed([b[4] for b in post], 60)
|
||||
first = next((i for i, x in enumerate(sm) if abs(x - 1) <= 0.10), None)
|
||||
res["first10_s"] = None if first is None else (post[first][0] - t_step) / 1000
|
||||
for i in range(len(post)):
|
||||
seg = sm[i:i + settle_blocks]
|
||||
if len(seg) == settle_blocks and all(abs(x - 1) <= 0.10 for x in seg):
|
||||
rec = post[i][0] - t_step
|
||||
break
|
||||
res["recover_s"] = None if rec is None else rec / 1000
|
||||
res["recover_blocks"] = None if rec is None else sum(1 for b in post if b[0] - t_step <= rec)
|
||||
r0 = post[0][4]
|
||||
side = 1 if r0 > 1 else -1
|
||||
cross = next((i for i, x in enumerate(sm) if (x - 1) * side <= 0), None)
|
||||
if cross is None:
|
||||
res["overshoot"] = 0.0
|
||||
else:
|
||||
tail = sm[cross:cross + 3600]
|
||||
res["overshoot"] = max(0.0, max((1 - x) * side for x in tail))
|
||||
res["above2x"] = sum(1 for b in post if b[4] > 2)
|
||||
res["below05"] = sum(1 for b in post if b[4] < 0.5)
|
||||
res["worst_gap_s"] = max((post[i][0] - post[i - 1][0]) for i in range(1, len(post))) / 1000 if len(post) > 1 else None
|
||||
return res
|
||||
|
||||
|
||||
def steady(blocks, t_from, t_to):
|
||||
seg = [b for b in blocks if t_from <= b[0] < t_to]
|
||||
if len(seg) < 10:
|
||||
return None, None, None
|
||||
rs = [b[4] for b in seg]
|
||||
mean = sum(rs) / len(rs)
|
||||
std = math.sqrt(sum((x - mean) ** 2 for x in rs) / len(rs))
|
||||
# blocks per minute
|
||||
mins = {}
|
||||
for b in seg:
|
||||
mins[int(b[0] // 60000)] = mins.get(int(b[0] // 60000), 0) + 1
|
||||
counts = list(mins.values())[1:-1]
|
||||
cm = sum(counts) / len(counts) if counts else 0
|
||||
cs = math.sqrt(sum((c - cm) ** 2 for c in counts) / len(counts)) if counts else 0
|
||||
gaps = [seg[i][0] - seg[i - 1][0] for i in range(1, len(seg))]
|
||||
return std, (cs / cm if cm else None), max(gaps) / 1000
|
||||
|
||||
|
||||
def fmt(x, nd=1):
|
||||
if x is None:
|
||||
return "none"
|
||||
if isinstance(x, float):
|
||||
return ("%." + str(nd) + "f") % x
|
||||
return str(x)
|
||||
|
||||
|
||||
def run_profile(profile, names, seed, ts_noise, kw=None):
|
||||
rows = []
|
||||
hour = 3600 * 1000
|
||||
for name in names:
|
||||
rng = random.Random(seed)
|
||||
ctrl = make_controller(name, ONE / profile.genesis_d, **(kw or {}))
|
||||
blocks = simulate(profile, ctrl, rng, ts_noise=ts_noise)
|
||||
row = {"ctrl": name, "blocks": len(blocks)}
|
||||
ev = profile.events()
|
||||
if profile.name.startswith("warmup"):
|
||||
m = metrics(blocks, profile, t_step=0)
|
||||
row.update(m)
|
||||
s = steady(blocks, 1 * hour, 2 * hour)
|
||||
elif profile.name == "epoch30":
|
||||
recs, ovs, a2, b5, wg = [], [], 0, 0, 0
|
||||
firsts = []
|
||||
for h, f in sorted(profile.epoch_steps.items()):
|
||||
t_end = next((b[0] for b in blocks if b[1] >= h + EPOCH), None)
|
||||
m = metrics(blocks, profile, h_step=h, t_end=t_end)
|
||||
recs.append(m.get("recover_s"))
|
||||
firsts.append(m.get("first10_s"))
|
||||
ovs.append(m.get("overshoot") or 0)
|
||||
a2 += m.get("above2x", 0)
|
||||
b5 += m.get("below05", 0)
|
||||
m = metrics(blocks, profile, h_step=2 * EPOCH)
|
||||
good = [r for r in recs if r is not None]
|
||||
gf = [r for r in firsts if r is not None]
|
||||
row["first10_s"] = (sum(gf) / len(gf)) if gf else None
|
||||
row["recover_s"] = (sum(good) / len(good)) if good else None
|
||||
row["recover_max_s"] = max(good) if good else None
|
||||
row["recover_fail"] = len(recs) - len(good)
|
||||
row["overshoot"] = max(ovs)
|
||||
row["above2x"] = sum(1 for b in blocks if b[4] > 2 and b[1] >= 2 * EPOCH)
|
||||
row["below05"] = sum(1 for b in blocks if b[4] < 0.5 and b[1] >= 2 * EPOCH)
|
||||
row["worst_gap_s"] = max(blocks[i][0] - blocks[i - 1][0] for i in range(1, len(blocks))) / 1000
|
||||
s = steady(blocks, 1 * hour, 2 * hour)
|
||||
elif profile.name.startswith("hop"):
|
||||
recs = []
|
||||
ovs = []
|
||||
firsts = []
|
||||
for i, (t, f) in enumerate(profile.time_steps):
|
||||
t_end = profile.time_steps[i + 1][0] if i + 1 < len(profile.time_steps) else None
|
||||
m = metrics(blocks, profile, t_step=t, t_end=t_end)
|
||||
recs.append(m.get("recover_s"))
|
||||
firsts.append(m.get("first10_s"))
|
||||
ovs.append(m.get("overshoot") or 0)
|
||||
good = [r for r in recs if r is not None]
|
||||
gf = [r for r in firsts if r is not None]
|
||||
row["first10_s"] = (sum(gf) / len(gf)) if gf else None
|
||||
row["recover_s"] = (sum(good) / len(good)) if good else None
|
||||
row["recover_max_s"] = max(good) if good else None
|
||||
row["recover_fail"] = len(recs) - len(good)
|
||||
row["overshoot"] = max(ovs)
|
||||
t0 = profile.time_steps[0][0]
|
||||
row["above2x"] = sum(1 for b in blocks if b[4] > 2 and b[0] >= t0)
|
||||
row["below05"] = sum(1 for b in blocks if b[4] < 0.5 and b[0] >= t0)
|
||||
row["worst_gap_s"] = max(blocks[i][0] - blocks[i - 1][0] for i in range(1, len(blocks)) if blocks[i][0] >= t0) / 1000
|
||||
s = steady(blocks, 1 * hour, 3 * hour)
|
||||
elif profile.name == "walk10":
|
||||
row["above2x"] = sum(1 for b in blocks if b[4] > 2 and b[0] >= hour)
|
||||
row["below05"] = sum(1 for b in blocks if b[4] < 0.5 and b[0] >= hour)
|
||||
row["worst_gap_s"] = max(blocks[i][0] - blocks[i - 1][0] for i in range(1, len(blocks)) if blocks[i][0] >= hour) / 1000
|
||||
s = steady(blocks, 1 * hour, 6 * hour)
|
||||
elif profile.name == "polluted":
|
||||
t_pc = profile.time_steps[2][0]
|
||||
m = metrics(blocks, profile, t_step=t_pc)
|
||||
row.update(m)
|
||||
post = [b for b in blocks if b[0] >= t_pc]
|
||||
row["peak_rate"] = max(b[4] for b in post)
|
||||
row["trough_rate"] = min(b[4] for b in post if b[0] >= t_pc + 600000) if any(b[0] >= t_pc + 600000 for b in post) else None
|
||||
s = steady(blocks, 3 * hour, 5 * hour)
|
||||
elif getattr(profile, "custom", False):
|
||||
recs, firsts, ovs = [], [], []
|
||||
for i, (t, f) in enumerate(profile.time_steps):
|
||||
t_end = profile.time_steps[i + 1][0] if i + 1 < len(profile.time_steps) else None
|
||||
m = metrics(blocks, profile, t_step=t, t_end=t_end)
|
||||
row["step%d_first" % (i + 1)] = m.get("first10_s")
|
||||
row["step%d_settled" % (i + 1)] = m.get("recover_s")
|
||||
row["step%d_gap" % (i + 1)] = m.get("worst_gap_s")
|
||||
row["blocks_within10"] = sum(1 for b in blocks if abs(b[4] - 1) <= 0.1)
|
||||
s = (None, None, None)
|
||||
elif profile.name == "real":
|
||||
segs = profile.segments
|
||||
# the biggest step in the record
|
||||
big = max(profile.time_steps, key=lambda e: abs(math.log(e[1])))
|
||||
m = metrics(blocks, profile, t_step=big[0])
|
||||
row.update(m)
|
||||
row["step"] = big[1]
|
||||
s = steady(blocks, segs[-1][0] + 600000, segs[-1][1]) if segs[-1][1] - segs[-1][0] > 1200000 else (None, None, None)
|
||||
row["blocks_within10"] = sum(1 for b in blocks if abs(b[4] - 1) <= 0.1)
|
||||
else:
|
||||
t, f = profile.time_steps[0]
|
||||
m = metrics(blocks, profile, t_step=t)
|
||||
row.update(m)
|
||||
s = steady(blocks, 1 * hour, 3 * hour)
|
||||
row["ss_std"], row["ss_bpm_cv"], row["ss_worst_gap_s"] = s
|
||||
if isinstance(ctrl, Igneum):
|
||||
row["engaged"] = ctrl.engaged
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
COLS = [("ctrl", "controller"), ("first10_s", "first within 10% s"), ("recover_s", "settled s"), ("recover_blocks", "recovery blocks"),
|
||||
("recover_max_s", "worst recovery s"), ("recover_fail", "not recovered"), ("overshoot", "overshoot"),
|
||||
("ss_std", "steady std of rate"), ("ss_bpm_cv", "steady blocks/min CV"), ("worst_gap_s", "worst gap s"),
|
||||
("above2x", "blocks above 2x"), ("below05", "blocks below 0.5x"), ("blocks", "blocks"), ("engaged", "fast-lane blocks"),
|
||||
("blocks_within10", "blocks within 10%"), ("step", "step factor"), ("peak_rate", "peak rate"), ("trough_rate", "trough rate after 10 min")]
|
||||
|
||||
|
||||
def print_table(title, rows):
|
||||
cols = [(k, h) for k, h in COLS if any(k in r for r in rows)]
|
||||
print("\n### " + title + "\n")
|
||||
print("| " + " | ".join(h for _, h in cols) + " |")
|
||||
print("|" + "---|" * len(cols))
|
||||
for r in rows:
|
||||
print("| " + " | ".join(fmt(r.get(k), 3 if k in ("ss_std", "ss_bpm_cv", "overshoot") else 1) for k, _ in cols) + " |")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--record")
|
||||
ap.add_argument("--seed", type=int, default=7)
|
||||
ap.add_argument("--ts-noise", type=int, default=0, help="uniform timestamp jitter, ms")
|
||||
ap.add_argument("--profiles", default="all")
|
||||
ap.add_argument("--controllers", default="kaspa,monero,lwma60,lwma120,igneum,igneum-literal")
|
||||
ap.add_argument("--tune", action="store_true")
|
||||
ap.add_argument("--events", default="", help="real record: comma list of ISO times of miner events")
|
||||
ap.add_argument("--dump", default="", help="profile:controller, write per-block trajectory to stdout as CSV and exit")
|
||||
ap.add_argument("--custom", default="", help="extra profile: name=genesis_bits_hex,base_MHs,duration_s,t_s:factor,t_s:factor,...")
|
||||
args = ap.parse_args()
|
||||
names = args.controllers.split(",")
|
||||
profiles = synthetic_profiles(args.seed)
|
||||
if args.profiles != "all":
|
||||
want = args.profiles.split(",")
|
||||
profiles = [p for p in profiles if p.name in want]
|
||||
if args.custom:
|
||||
name, rest = args.custom.split("=")
|
||||
parts = rest.split(",")
|
||||
gbits, mhs, dur = int(parts[0], 16), float(parts[1]), float(parts[2])
|
||||
p = Profile(name, mhs * 1e6 / 1000, dur * 1000, genesis_d=ONE / bits_to_target(gbits), seed=args.seed)
|
||||
p.time_steps = [(float(x.split(":")[0]) * 1000, float(x.split(":")[1])) for x in parts[3:]]
|
||||
p.custom = True
|
||||
profiles.append(p)
|
||||
if args.dump:
|
||||
pn, cn = args.dump.split(":")
|
||||
p = next(q for q in synthetic_profiles(args.seed) if q.name == pn)
|
||||
ctrl = make_controller(cn, ONE / p.genesis_d)
|
||||
for t, h, D, hr, r in simulate(p, ctrl, random.Random(args.seed)):
|
||||
print("%d,%d,%.6g,%.6g,%.4f" % (t, h, D, hr, r))
|
||||
return
|
||||
print("# Difficulty controller simulation, seed %d, ts noise %d ms\n" % (args.seed, args.ts_noise))
|
||||
if args.tune:
|
||||
p_names = ["up50", "down50", "epoch30", "hop10", "walk10", "polluted"]
|
||||
base = {}
|
||||
grid = [("ns", [60, 120, 180]), ("trig", [0.15, 0.25, 0.35]), ("clamp_pct", [0.02, 0.03, 0.05]),
|
||||
("ease_pct", [0.03, 0.06, 0.10, 0.15]),
|
||||
("k0", [8, 16, 32]), ("cap", [6 * T, 20 * T, FTL])]
|
||||
for key, vals in grid:
|
||||
rows = []
|
||||
for v in vals:
|
||||
kw = dict(base)
|
||||
kw[key] = v
|
||||
agg = {"ctrl": "%s=%s" % (key, v if key != "cap" else v // 1000)}
|
||||
for p in [q for q in synthetic_profiles(args.seed) if q.name in p_names]:
|
||||
r = run_profile(p, ["igneum"], args.seed, args.ts_noise, kw)[0]
|
||||
agg[p.name + " rec"] = r.get("recover_s")
|
||||
agg[p.name + " first"] = r.get("first10_s")
|
||||
agg[p.name + " std"] = r.get("ss_std")
|
||||
if p.name == "down50":
|
||||
agg["down50 gap"] = r.get("worst_gap_s")
|
||||
if p.name == "polluted":
|
||||
agg["polluted peak"] = r.get("peak_rate")
|
||||
if p.name == "walk10":
|
||||
agg["walk10 >2x/<0.5x"] = "%d/%d" % (r.get("above2x", 0), r.get("below05", 0))
|
||||
rows.append(agg)
|
||||
keys = list(rows[0].keys())
|
||||
print("\n### Tune %s\n" % key)
|
||||
print("| " + " | ".join(keys) + " |")
|
||||
print("|" + "---|" * len(keys))
|
||||
for r in rows:
|
||||
print("| " + " | ".join(fmt(r[k], 3 if k.endswith("std") else 1) for k in keys) + " |")
|
||||
return
|
||||
for p in profiles:
|
||||
rows = run_profile(p, names, args.seed, args.ts_noise)
|
||||
if getattr(p, "custom", False):
|
||||
continue
|
||||
desc = {"up50": "hash rate x50 at 3 h", "down50": "hash rate /50 at 3 h",
|
||||
"epoch30": "x1.3 or /1.3 at each epoch boundary from epoch 2", "walk10": "10% per hour log random walk",
|
||||
"hop3": "3x pool hops in and out every 15 min from 3 h", "hop10": "10x pool hops in and out every 15 min from 3 h",
|
||||
"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)", "warmup-hard": "genesis difficulty 10x too hard", "warmup-easy": "genesis difficulty 10x too easy"}[p.name]
|
||||
print_table("%s: %s" % (p.name, desc), rows)
|
||||
for p in profiles:
|
||||
if getattr(p, "custom", False):
|
||||
rows = run_profile(p, names, args.seed, args.ts_noise)
|
||||
keys = [k for k in rows[0].keys() if k != "ss_std" and k != "ss_bpm_cv" and k != "ss_worst_gap_s"]
|
||||
print("\n### %s: custom profile\n" % p.name)
|
||||
print("| " + " | ".join(keys) + " |")
|
||||
print("|" + "---|" * len(keys))
|
||||
for r in rows:
|
||||
print("| " + " | ".join(fmt(r.get(k)) for k in keys) + " |")
|
||||
if args.record:
|
||||
rec, every = load_record(args.record)
|
||||
checked, mism, first_bad, exact = kaspa_bits_exact(rec)
|
||||
print("\n### Exact replay of Kaspa's rule on the record\n")
|
||||
print("Blocks %d, chain blocks %d, retargets checked %d, exact before the first mismatch %d, mismatches %d%s" % (
|
||||
len(every), len(rec), checked, exact, mism, "" if first_bad is None else ", first at daa %d: have %s want %s" % first_bad))
|
||||
print("(A chain-only replay cannot see the merged blocks that Kaspa's sampled window includes once the DAG widens.)")
|
||||
import datetime
|
||||
ev = []
|
||||
for s in [x for x in args.events.split(",") if x]:
|
||||
ev.append((int(datetime.datetime.fromisoformat(s).timestamp() * 1000), s))
|
||||
p = record_profile(rec, every, sorted(ev), args.seed)
|
||||
print("\nReal profile segments (hash rate estimated as expected hashes over wall time):\n")
|
||||
print("| from s | to s | blocks/s at genesis difficulty | MH/s |")
|
||||
print("|---|---|---|---|")
|
||||
g = p.genesis_d
|
||||
for a, b, r in p.segments:
|
||||
print("| %d | %d | %.3f | %.1f |" % (a / 1000, b / 1000, r * T / (2 * g), r * 1000 / 1e6))
|
||||
rows = run_profile(p, names, args.seed, args.ts_noise)
|
||||
print_table("real: the devnet record replayed as a hash-rate profile", rows)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1135
sim/difficulty/testnet-igneum-2026-10-03.csv
Normal file
1135
sim/difficulty/testnet-igneum-2026-10-03.csv
Normal file
File diff suppressed because it is too large
Load diff
72
sim/difficulty/testnet-kaspa-2026-10-03.csv
Normal file
72
sim/difficulty/testnet-kaspa-2026-10-03.csv
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
hash,daa_score,blue_score,timestamp_ms,bits,difficulty,target_log2,epoch,is_chain_block,parents,selected_parent
|
||||
f682b78a4e64f0d21c7978af0d341aef55ae30bdea750f8f5d8ac715bb46e099,0,0,1790985600000,0x1e200000,262143,237.000,0,1,0,fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe
|
||||
0322b4a319b626591c8757e6dd37cd2a0101ac60ca4fec5e11d78bbfc5e49377,0,1,1791064841899,0x1e200000,262143,237.000,0,1,1,f682b78a4e64f0d21c7978af0d341aef55ae30bdea750f8f5d8ac715bb46e099
|
||||
8d513cd789b7b1e346ceb775ca9e6e494e7159f83495059baecc6469690c7dfd,1,2,1791064845785,0x1e200000,262143,237.000,0,1,1,0322b4a319b626591c8757e6dd37cd2a0101ac60ca4fec5e11d78bbfc5e49377
|
||||
737a7b5f6eabf30ebe329d71485e0fa9c25300f45cc45bd60e7bb6b1be9e0cfd,2,3,1791064851888,0x1e200000,262143,237.000,0,1,1,8d513cd789b7b1e346ceb775ca9e6e494e7159f83495059baecc6469690c7dfd
|
||||
bd97690b8210e032bd894f3c79377abafae22b3c669dc63f3c15ed45eb2514a6,3,4,1791064866516,0x1e200000,262143,237.000,0,1,1,737a7b5f6eabf30ebe329d71485e0fa9c25300f45cc45bd60e7bb6b1be9e0cfd
|
||||
c62206a65591599ee931b606cdbca8fd0bc654dabf25e16653603f7811a54998,4,5,1791064869993,0x1e200000,262143,237.000,0,1,1,bd97690b8210e032bd894f3c79377abafae22b3c669dc63f3c15ed45eb2514a6
|
||||
60559f78fbecf1f1d2f57a002382fb2d1d40bedcc71901eb27d2f759315d4cbd,5,6,1791064870816,0x1e200000,262143,237.000,0,1,1,c62206a65591599ee931b606cdbca8fd0bc654dabf25e16653603f7811a54998
|
||||
620fa01ae50573d9829e94e02db41736ba075667e1ab4922bc90996d6e981cb4,6,7,1791064877486,0x1e200000,262143,237.000,0,1,1,60559f78fbecf1f1d2f57a002382fb2d1d40bedcc71901eb27d2f759315d4cbd
|
||||
c7000d7537787bd32713dcfd9e585144287ebde7729fd0a9113176aba7cce656,7,8,1791064878412,0x1e200000,262143,237.000,0,1,1,620fa01ae50573d9829e94e02db41736ba075667e1ab4922bc90996d6e981cb4
|
||||
3575023193a84235ee38a5b71ed249b605fd03aa94c4b35594237e8a4d9fa849,8,9,1791064880523,0x1e200000,262143,237.000,0,1,1,c7000d7537787bd32713dcfd9e585144287ebde7729fd0a9113176aba7cce656
|
||||
5cec2c6908fb88fd48e5fbf95ab85cc2eeaf1d71bd1d8b77b8c9b363a4ef4641,9,10,1791064881901,0x1e200000,262143,237.000,0,1,1,3575023193a84235ee38a5b71ed249b605fd03aa94c4b35594237e8a4d9fa849
|
||||
6fa137426b9941e6dc6a429a8eb6f0c9b4dac6c6ce279cc357ef72ceec4fd6b5,10,11,1791064887677,0x1e200000,262143,237.000,0,1,1,5cec2c6908fb88fd48e5fbf95ab85cc2eeaf1d71bd1d8b77b8c9b363a4ef4641
|
||||
fec7a670e822c1f08b14407f48ac8a473ddb46189abdace2bd927d0eabeeaf46,11,12,1791064889947,0x1e200000,262143,237.000,0,1,1,6fa137426b9941e6dc6a429a8eb6f0c9b4dac6c6ce279cc357ef72ceec4fd6b5
|
||||
c03383e906bf2f321d79b4e7efe097056c53469c3058462cf93039a19d57543c,12,13,1791064900681,0x1e200000,262143,237.000,0,1,1,fec7a670e822c1f08b14407f48ac8a473ddb46189abdace2bd927d0eabeeaf46
|
||||
3e1b589dbe8faa0461f4cb3b8b33d29980ed2f05c3b3f3d15b04768f592dd2f5,13,14,1791064902336,0x1e200000,262143,237.000,0,1,1,c03383e906bf2f321d79b4e7efe097056c53469c3058462cf93039a19d57543c
|
||||
a0cd09bc47c9ed43d225988c06a8cc346484c9b71134a64b56613af6466aab2a,14,15,1791064911067,0x1e200000,262143,237.000,0,1,1,3e1b589dbe8faa0461f4cb3b8b33d29980ed2f05c3b3f3d15b04768f592dd2f5
|
||||
08a85eaf1ff27d9d325f7554a5988952f5e562389f8dcfcfce40af43c48fa209,15,16,1791064918039,0x1e200000,262143,237.000,0,1,1,a0cd09bc47c9ed43d225988c06a8cc346484c9b71134a64b56613af6466aab2a
|
||||
41b30a0cbf2a158d8e491fc6290cfcfe4a2bce32eb613fd15c8b31abca3208fc,16,17,1791064919174,0x1e200000,262143,237.000,0,1,1,08a85eaf1ff27d9d325f7554a5988952f5e562389f8dcfcfce40af43c48fa209
|
||||
1e836535a3ec1b6658ee6b20c28d03f24c509d4dabc9d6faac8a687f567990b4,17,18,1791064921291,0x1e200000,262143,237.000,0,1,1,41b30a0cbf2a158d8e491fc6290cfcfe4a2bce32eb613fd15c8b31abca3208fc
|
||||
be41f51e9a963987eac903aed6e33d7b7c22a15d18c5f679152876bc00d5e034,18,19,1791064923900,0x1e200000,262143,237.000,0,1,1,1e836535a3ec1b6658ee6b20c28d03f24c509d4dabc9d6faac8a687f567990b4
|
||||
f9d894fc96d4b90a936428ce5e81602174947c3bfcf16f5f1814c0b3a506a049,19,20,1791064927151,0x1e200000,262143,237.000,0,1,1,be41f51e9a963987eac903aed6e33d7b7c22a15d18c5f679152876bc00d5e034
|
||||
1ffe476303b0e413e3351465754aa4acd0d4f11277ea378eb5b0bb7397a81a2b,20,21,1791064943449,0x1e200000,262143,237.000,0,1,1,f9d894fc96d4b90a936428ce5e81602174947c3bfcf16f5f1814c0b3a506a049
|
||||
c147e6657d92dc29972dcf85988856fb57bb933b73e564684d0216ca948f82f4,21,22,1791064945254,0x1e200000,262143,237.000,0,1,1,1ffe476303b0e413e3351465754aa4acd0d4f11277ea378eb5b0bb7397a81a2b
|
||||
b261d241f3ddf5bc60fdb3e6cdadc2433e94f726c6f8ff69458601fd07d46cf6,22,23,1791064950482,0x1e200000,262143,237.000,0,1,1,c147e6657d92dc29972dcf85988856fb57bb933b73e564684d0216ca948f82f4
|
||||
35e97c3f2b24223c908fd76af68dbb5867d8f14b4547107b719960cc44e7f49b,23,24,1791064958922,0x1e200000,262143,237.000,0,1,1,b261d241f3ddf5bc60fdb3e6cdadc2433e94f726c6f8ff69458601fd07d46cf6
|
||||
fb5bbdb7d72646bd03e48b5b974b0931cd7162661520396481603889c52e2d91,24,25,1791064973190,0x1e200000,262143,237.000,0,1,1,35e97c3f2b24223c908fd76af68dbb5867d8f14b4547107b719960cc44e7f49b
|
||||
a1cb9b1e9bc6c8572a9262a87ec0e4ad67cb91cd80671d2721f28de3a7420803,25,26,1791064985334,0x1e200000,262143,237.000,0,1,1,fb5bbdb7d72646bd03e48b5b974b0931cd7162661520396481603889c52e2d91
|
||||
d761677b75518c1696bdc4e8c3da23da337855719f0c4b837fbf2bdc5a67255e,26,27,1791064988846,0x1e200000,262143,237.000,0,1,1,a1cb9b1e9bc6c8572a9262a87ec0e4ad67cb91cd80671d2721f28de3a7420803
|
||||
accdf3d7f1e05c4ccf4abe1ddfa2e8ee321c10dc4e2632ec6ed0e29b3bd33534,27,28,1791064990073,0x1e200000,262143,237.000,0,1,1,d761677b75518c1696bdc4e8c3da23da337855719f0c4b837fbf2bdc5a67255e
|
||||
0a287ebbccd3702ef0f9ae0b1a16e96235c9ffed351a6c5d729e35fa91c4e26e,28,29,1791064991359,0x1e200000,262143,237.000,0,1,1,accdf3d7f1e05c4ccf4abe1ddfa2e8ee321c10dc4e2632ec6ed0e29b3bd33534
|
||||
f9148a720809f21174eb868a8693be44cf6107eb86c61bd3c8627729f9132fce,29,30,1791064991825,0x1e200000,262143,237.000,0,1,1,0a287ebbccd3702ef0f9ae0b1a16e96235c9ffed351a6c5d729e35fa91c4e26e
|
||||
054796677bd96555ffeb9ebb976e7e9debdb0f844329306f32db421ae4915701,30,31,1791064999476,0x1e200000,262143,237.000,0,1,1,f9148a720809f21174eb868a8693be44cf6107eb86c61bd3c8627729f9132fce
|
||||
e81ff6542c6db03010fb2d28818bb8af31dafecb9b03aa6a066c6d677887a795,31,32,1791065002853,0x1e200000,262143,237.000,0,1,1,054796677bd96555ffeb9ebb976e7e9debdb0f844329306f32db421ae4915701
|
||||
e6949659f12c8e7977948d1424b8d45f290f59bc2b8664488c0daac28f07012f,32,33,1791065003701,0x1e200000,262143,237.000,0,1,1,e81ff6542c6db03010fb2d28818bb8af31dafecb9b03aa6a066c6d677887a795
|
||||
17ede982c9bfb3f4596784fe8015da4cc0e1c64db3de5cf7aa3f7140a2b2ed52,33,34,1791065006811,0x1e200000,262143,237.000,0,1,1,e6949659f12c8e7977948d1424b8d45f290f59bc2b8664488c0daac28f07012f
|
||||
a8500c0de976454edaf793a7d39d444f3d4322c2263277e6efef2124f456081f,34,35,1791065008031,0x1e200000,262143,237.000,0,1,1,17ede982c9bfb3f4596784fe8015da4cc0e1c64db3de5cf7aa3f7140a2b2ed52
|
||||
c99d13b3187b2bd58081ba9f9f8cfdf814ef2cb4be1c5113e21dd8bced9da38d,35,36,1791065020076,0x1e200000,262143,237.000,0,1,1,a8500c0de976454edaf793a7d39d444f3d4322c2263277e6efef2124f456081f
|
||||
1f77783a4d3c9a281858097a38637238b9f40f5815a1230b928001a7021c389a,36,37,1791065026806,0x1e200000,262143,237.000,0,1,1,c99d13b3187b2bd58081ba9f9f8cfdf814ef2cb4be1c5113e21dd8bced9da38d
|
||||
2c712b8ae345b2e810df9751a5ec50c39b7a00443807fbbfc02e9a8bf1e9a726,37,38,1791065030951,0x1e200000,262143,237.000,0,1,1,1f77783a4d3c9a281858097a38637238b9f40f5815a1230b928001a7021c389a
|
||||
fe5ddb5b18283727c8be65baaabab2d41c50879c9e36e20e16fd27f6f66ab4c1,38,39,1791065035617,0x1e200000,262143,237.000,0,1,1,2c712b8ae345b2e810df9751a5ec50c39b7a00443807fbbfc02e9a8bf1e9a726
|
||||
f2fa37cc103d237778e9313ce6e8d2153d696fb4625997eca76d330c354f7ed0,39,40,1791065039405,0x1e200000,262143,237.000,0,1,1,fe5ddb5b18283727c8be65baaabab2d41c50879c9e36e20e16fd27f6f66ab4c1
|
||||
2d4b40be333452a6f8902c1794806245946b78ff58f4784c590c83f421305f35,40,41,1791065046855,0x1e200000,262143,237.000,0,1,1,f2fa37cc103d237778e9313ce6e8d2153d696fb4625997eca76d330c354f7ed0
|
||||
b5f8bf309a9a382670f745b023b5a4ebaa8e4e1bba5c47b4dabcc1a92d30b1ec,41,42,1791065050857,0x1e200000,262143,237.000,0,1,1,2d4b40be333452a6f8902c1794806245946b78ff58f4784c590c83f421305f35
|
||||
48ca377d34e136796de5157b5c3b0ce14eac73e2399e3a6afc4627a9d1cbb7a6,42,43,1791065052476,0x1e200000,262143,237.000,0,1,1,b5f8bf309a9a382670f745b023b5a4ebaa8e4e1bba5c47b4dabcc1a92d30b1ec
|
||||
e2b56dad6d08493dc2d5aeab31e9f3e8dc8c191cfa27036481bcedf3241e1860,43,44,1791065060489,0x1e200000,262143,237.000,0,1,1,48ca377d34e136796de5157b5c3b0ce14eac73e2399e3a6afc4627a9d1cbb7a6
|
||||
9d11abb454f2e37c0cd22c124859ae328998a653aeea8be7e3a7659fa0dd63c3,44,45,1791065061231,0x1e200000,262143,237.000,0,1,1,e2b56dad6d08493dc2d5aeab31e9f3e8dc8c191cfa27036481bcedf3241e1860
|
||||
eb8294eabb1cc9ce45672ec6f0ca70594ab4a0090c23bce3373181b9a849874c,45,46,1791065074683,0x1e200000,262143,237.000,0,1,1,9d11abb454f2e37c0cd22c124859ae328998a653aeea8be7e3a7659fa0dd63c3
|
||||
e171a36ec1db1c80f10be66dbcfc040280f23045062ddbbeaf244dfcecc50c52,46,47,1791065076616,0x1e200000,262143,237.000,0,1,1,eb8294eabb1cc9ce45672ec6f0ca70594ab4a0090c23bce3373181b9a849874c
|
||||
3dd1e017f956d795cf38afa8b9d4fecbe946f2235e6a3b707e035cbdb2b9149a,47,48,1791065077978,0x1e200000,262143,237.000,0,1,1,e171a36ec1db1c80f10be66dbcfc040280f23045062ddbbeaf244dfcecc50c52
|
||||
48082590e3b3a9a26b342b30c59e8ef1fb5c8504a4ce4d2d80861e60bdc61522,48,49,1791065089796,0x1e200000,262143,237.000,0,1,1,3dd1e017f956d795cf38afa8b9d4fecbe946f2235e6a3b707e035cbdb2b9149a
|
||||
07255ff6f23c535ab57649fbebe4a2f77c3ae2bf4693481b49039b0ec2c7fd47,49,50,1791065090108,0x1e200000,262143,237.000,0,1,1,48082590e3b3a9a26b342b30c59e8ef1fb5c8504a4ce4d2d80861e60bdc61522
|
||||
ab5bf826fe7448ec3c9972646a44361dd756bb040db04904a2c3c9830689e018,50,51,1791065094402,0x1e200000,262143,237.000,0,1,1,07255ff6f23c535ab57649fbebe4a2f77c3ae2bf4693481b49039b0ec2c7fd47
|
||||
675447226354a45e35b1b1febfa936dd5731ef428d012c8c10a9b07f9a0b2c04,51,52,1791065130757,0x1e200000,262143,237.000,0,1,1,ab5bf826fe7448ec3c9972646a44361dd756bb040db04904a2c3c9830689e018
|
||||
d6220f9c1ec76d2bb5c86038b27c0d15f57be979902b40df7cd1bf11ce4c9ef1,52,53,1791065147386,0x1e200000,262143,237.000,0,1,1,675447226354a45e35b1b1febfa936dd5731ef428d012c8c10a9b07f9a0b2c04
|
||||
f949325b145a3b62e7af1536eb3e9f48506bd772bff91994cb9aa37e18e78d25,53,54,1791065154993,0x1e200000,262143,237.000,0,1,1,d6220f9c1ec76d2bb5c86038b27c0d15f57be979902b40df7cd1bf11ce4c9ef1
|
||||
6c19660fcccf3567c08028cbe732117bd4d9f879c1399e8e94b81d7100017428,54,55,1791065158489,0x1e200000,262143,237.000,0,1,1,f949325b145a3b62e7af1536eb3e9f48506bd772bff91994cb9aa37e18e78d25
|
||||
b0c456d7bbc1e3c12ec07e93838e491d8b7c63a21c1d7b6bc8835c02bd8f09cf,55,56,1791065170560,0x1e200000,262143,237.000,0,1,1,6c19660fcccf3567c08028cbe732117bd4d9f879c1399e8e94b81d7100017428
|
||||
8365a9bb74f2d1dc38e8bde28bf0a2a85578a2f2d68be7f885dcdd287c61a9e6,56,57,1791065183725,0x1e200000,262143,237.000,0,1,1,b0c456d7bbc1e3c12ec07e93838e491d8b7c63a21c1d7b6bc8835c02bd8f09cf
|
||||
7d48c5d69d6b4bd376df1f96d03ee9d6d835abbf7f4b8c466a7517a02155709c,57,58,1791065186929,0x1e200000,262143,237.000,0,1,1,8365a9bb74f2d1dc38e8bde28bf0a2a85578a2f2d68be7f885dcdd287c61a9e6
|
||||
a707d2cd94132978bdc7edea6c9b0e4fb0bc61e1522a6e7634d699d1d566e0fd,58,59,1791065205228,0x1e200000,262143,237.000,0,1,1,7d48c5d69d6b4bd376df1f96d03ee9d6d835abbf7f4b8c466a7517a02155709c
|
||||
e8f83031bdcf103cc198195b502b75111eb2cd5cd84dbefe88915c9029fac73f,59,60,1791065210983,0x1e200000,262143,237.000,0,1,1,a707d2cd94132978bdc7edea6c9b0e4fb0bc61e1522a6e7634d699d1d566e0fd
|
||||
9ccc5bc3e825d5c4caec2d4f988e75d1186f602a3311cfbc82fc60b247037676,60,61,1791065235129,0x1e200000,262143,237.000,0,1,1,e8f83031bdcf103cc198195b502b75111eb2cd5cd84dbefe88915c9029fac73f
|
||||
84351ab4fa287b444ee9ca53ed29c86b7e03f1e8041ec4335c0d519ba12dda96,61,62,1791065240447,0x1e200000,262143,237.000,0,1,1,9ccc5bc3e825d5c4caec2d4f988e75d1186f602a3311cfbc82fc60b247037676
|
||||
dba4a780d02c6403a7f0f4fb3f97cc5e8c656510c656cc0b2414e5820d74c6ae,62,63,1791065241055,0x1e200000,262143,237.000,0,1,1,84351ab4fa287b444ee9ca53ed29c86b7e03f1e8041ec4335c0d519ba12dda96
|
||||
6302c364ca17963f38cc8cc58bd48ab31abd680d0a0d18f9a1bcff7ef4bc59f2,63,64,1791065242751,0x1e200000,262143,237.000,0,1,1,dba4a780d02c6403a7f0f4fb3f97cc5e8c656510c656cc0b2414e5820d74c6ae
|
||||
7cc366e9d92bbd164f2b993d148b800c9527baf873bb923843deb28a96d9f899,64,65,1791065296547,0x1e200000,262143,237.000,0,1,1,6302c364ca17963f38cc8cc58bd48ab31abd680d0a0d18f9a1bcff7ef4bc59f2
|
||||
053e38976f96c4c626118cb58f4374f48f454ff4cbf907ae77ebbf05abf5e796,65,66,1791065308412,0x1e200000,262143,237.000,0,1,1,7cc366e9d92bbd164f2b993d148b800c9527baf873bb923843deb28a96d9f899
|
||||
bf028090f1acd0e1d3267787a988da59ef835be47f2d8d5e1a33be673db94ab3,66,67,1791065323011,0x1e200000,262143,237.000,0,1,1,053e38976f96c4c626118cb58f4374f48f454ff4cbf907ae77ebbf05abf5e796
|
||||
e3f6898abe54a323ee14c9dbd413a03b63120aae2cebe5854ebf03effd648012,67,68,1791065386033,0x1e200000,262143,237.000,0,1,1,bf028090f1acd0e1d3267787a988da59ef835be47f2d8d5e1a33be673db94ab3
|
||||
b5ded0f901dc18df095fdeacacb213bc08ee44b322b095b3fac8bc46260210ad,68,69,1791065403843,0x1e200000,262143,237.000,0,1,1,e3f6898abe54a323ee14c9dbd413a03b63120aae2cebe5854ebf03effd648012
|
||||
ea9432a210ce50f7556b17cfec9509892dcdb732c5e3b3dff4bd098965801fe0,69,70,1791065407394,0x1e200000,262143,237.000,0,1,1,b5ded0f901dc18df095fdeacacb213bc08ee44b322b095b3fac8bc46260210ad
|
||||
|
Loading…
Reference in a new issue