Counter ASIC 3.0 gates (node): the seven-window signal gate re-run on fork 0760b844 (four cases, section 6.5), the controller harness for difficulty rule v3, the getRecentBlocks API row

class-v4-signal.mjs now runs the 0.3.16 rule (95 percent in each of 7 consecutive windows ending at the seed block;
the flip at epoch 4 with the 30-DAA window), requires a floor for a signalling case (the canary rule: no floor, no
signal; the floor-less first pass read every chain byte 0), reads programClassV4SignalWeakestBps and checks that the
signal line names seven windows. The four cases on 6 October 2026, 20:57 to 21:25Z: two of three signal, no flip
(weakest 5,666 to 6,333 bps); all three, flip at epoch 4 (DAA 241 seen, boundary 240; 3 of 3 signal lines; the id
assertion on epochs 4 to 6); nobody, the floor at 360, flip at epoch 6 and not before; the known-failed case FAILS on
nine checks. Summaries in docs/plans/counter-asic-3-gate/class-v4-signal7-*.json.

infra/fast-time/difficulty-v3.mjs (Horizon lane 5 proposal 2's harness case): three nodes in a star through a TCP
delay proxy (the document's propagation delay d over Kaspa's three relay legs, inv, request, block, so d / 3 per
leg; the handshake's first 5 s undelayed, the version exchange times out at 4 s), override-60x.json re-rated to
10 bps (the override-params.md example's k 124 and limits, depths x10, PoW epoch 600 DAA so an epoch stays 60 s),
one CPU miner per node, pass = DAG rate within 10 percent of target and the sink's expected hashes per block within
10 percent of the miners' summed hash x T after 600 of 900 s; the red share and chain step spacing reported. Results
follow in their own commit (the runs are in progress at 21:30Z).

docs/design/execution-layer.md 8.2: the igneum_getRecentBlocks row (fork eec34ac3).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 21:25:58 +00:00
parent a06c708bf6
commit 20a4841030
8 changed files with 2578 additions and 16 deletions

View file

@ -367,6 +367,7 @@ Standard namespace, unchanged semantics where the table says so; RPC "blocks" ar
| `igneum_getSegment(number)` | New | Mergeset in order, executed set, shard plan, proof record if any |
| `igneum_getTransactionStatus(hash)` | New | `{included_in: [hashes], executing_copy, executed, proven, locked, skip_reason}` |
| `igneum_getProvingFee`, `igneum_getBudgets` | New | `f_p`, `B_e`, `B_p`, backlog depth |
| `igneum_getRecentBlocks(seconds)` | New (0.3.16) | Every block the executed chain saw in the last `seconds` (default and cap 600) by chain-block header time, newest chain block first, its merged blocks after it in mergeset order: `{hash, blue_score, daa_score, timestamp_ms, vote_key_hash, is_chain_block, color, timestamp_source}`; `color` blue or red; a merged row's time and blue score come from the header store (`timestamp_source` "header") or, without consensus, from its chain block ("chain_block"). The app's live view derives its 10 minute miner count and per minute buckets from one reply |
### 8.3 Hardhat and Foundry

View file

@ -0,0 +1,559 @@
{
"pass": false,
"expect": "flip",
"signals": [
4,
4,
3
],
"checks": {
"zero_rejected_by_miners": true,
"zero_rejected_by_nodes": true,
"sinks_agree": true,
"block_counts_agree": true,
"miners_agree_on_every_program": true,
"window_line_on_every_node": true,
"every_node_signals_its_byte": true,
"chain_carries_the_bytes": true,
"template_switched_to_v4": false,
"switched_at_the_first_full_window_epoch": false,
"switched_before_the_floor": false,
"signal_line_on_every_node_same_epoch": false,
"signal_share_at_or_above_threshold": false,
"signal_line_names_seven_windows": false,
"blocks_on_both_sides": false,
"v4_ids_equal_the_cli_v4_id": false,
"v4_ids_differ_from_the_same_seed_v3_id": false
},
"window": 30,
"windows": 7,
"floor": 100000,
"v3_activation": 60,
"epoch_blocks": 60,
"lead": 10,
"first_full_window_epoch": 4,
"floor_epoch": 1667,
"node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd",
"miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner",
"template_switch": null,
"run_ended_at_s": 410.2,
"final_daa": 421,
"max_epoch_seen": 7,
"epochs": {
"0": {
"class": 2,
"firstSeenDaa": 0,
"at": 4.6,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"1": {
"class": 3,
"firstSeenDaa": 60,
"at": 27.7,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"2": {
"class": 3,
"firstSeenDaa": 120,
"at": 91.8,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"3": {
"class": 3,
"firstSeenDaa": 180,
"at": 160.9,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"4": {
"class": 3,
"firstSeenDaa": 240,
"at": 233,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 5333,
"signal_epoch": null
},
"5": {
"class": 3,
"firstSeenDaa": 300,
"at": 293.1,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 5333,
"signal_epoch": null
},
"6": {
"class": 3,
"firstSeenDaa": 360,
"at": 350.2,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 5333,
"signal_epoch": null
},
"7": {
"class": 3,
"firstSeenDaa": 421,
"at": 410.2,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 6333,
"signal_epoch": null
}
},
"blocks": {
"total": 424,
"before_boundary": 424,
"after_boundary": 0,
"version_bytes": {
"0": 1,
"3": 135,
"4": 288
},
"signal_share_bps_on_chain": 6792
},
"programs": [
{
"epoch": 0,
"class": "v2",
"program_id": "8f8806638d59850f",
"seed": "234e082d653dc69d",
"miners": 3,
"disagree": false
},
{
"epoch": 1,
"class": "v3",
"program_id": "7e2f9a15ca615321",
"seed": "ae6cdc46ffc6ac68",
"miners": 3,
"disagree": false
},
{
"epoch": 2,
"class": "v3",
"program_id": "2c2a98fae78f74b8",
"seed": "7d44b28f7a9c7f0a",
"miners": 3,
"disagree": false
},
{
"epoch": 3,
"class": "v3",
"program_id": "89ae54d5b5eae763",
"seed": "6aea4563d3d935b3",
"miners": 3,
"disagree": false
},
{
"epoch": 4,
"class": "v3",
"program_id": "5a0ca4f1ff74e70c",
"seed": "c37cf6f9e8cd33b5",
"miners": 3,
"disagree": false
},
{
"epoch": 5,
"class": "v3",
"program_id": "e4af5e0c4d9b93c2",
"seed": "f8c304d64d5f8225",
"miners": 3,
"disagree": false
},
{
"epoch": 6,
"class": "v3",
"program_id": "b5ca5848f6f0413b",
"seed": "1eb6aa4c17a2eed4",
"miners": 3,
"disagree": false
},
{
"epoch": 7,
"class": "v3",
"program_id": "ab61dbf422b32bc5",
"seed": "9532201279474928",
"miners": 3,
"disagree": false
}
],
"program_id_rows": [],
"accepted_per_miner": [
131,
157,
135
],
"rejected_by_miners": [
0,
0,
0
],
"rejected_by_nodes": [
0,
0,
0
],
"sinks": [
"1c1eae6d5c82de1c",
"1c1eae6d5c82de1c",
"1c1eae6d5c82de1c"
],
"block_counts": [
423,
423,
423
],
"signal_lines": [
null,
null,
null
],
"floor_lines": [
"Program class v4 from the override file: active from epoch 1667 (DAA score 100000 rounded up to the epoch boundary at 100020, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 1667 (DAA score 100000 rounded up to the epoch boundary at 100020, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 1667 (DAA score 100000 rounded up to the epoch boundary at 100020, epochs of 60 DAA)"
],
"samples": [
{
"t": 4.6,
"daa": 0,
"epoch": 0,
"class": 2,
"bps": 0,
"nodes": [
"0/234e082d",
"0/234e082d",
"0/234e082d"
]
},
{
"t": 19.7,
"daa": 47,
"epoch": 0,
"class": 2,
"bps": 0,
"nodes": [
"47/9003a9d9",
"47/9003a9d9",
"47/9003a9d9"
]
},
{
"t": 34.7,
"daa": 64,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"64/b4c1d83f",
"64/b4c1d83f",
"64/b4c1d83f"
]
},
{
"t": 49.7,
"daa": 69,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"69/3dda5305",
"69/3dda5305",
"69/3dda5305"
]
},
{
"t": 64.8,
"daa": 88,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"88/84d103be",
"88/84d103be",
"88/84d103be"
]
},
{
"t": 79.8,
"daa": 107,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"107/95682fc8",
"107/95682fc8",
"107/95682fc8"
]
},
{
"t": 94.8,
"daa": 120,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"120/bb25b3af",
"120/bb25b3af",
"120/bb25b3af"
]
},
{
"t": 109.8,
"daa": 138,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"138/82254c76",
"138/82254c76",
"138/82254c76"
]
},
{
"t": 124.9,
"daa": 154,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"154/ab3e9744",
"154/ab3e9744",
"154/ab3e9744"
]
},
{
"t": 139.9,
"daa": 167,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"167/381fdc27",
"167/381fdc27",
"167/381fdc27"
]
},
{
"t": 154.9,
"daa": 174,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"174/b6930be6",
"174/b6930be6",
"174/b6930be6"
]
},
{
"t": 169.9,
"daa": 190,
"epoch": 3,
"class": 3,
"bps": 4285,
"nodes": [
"190/3473bc22",
"190/3473bc22",
"190/3473bc22"
]
},
{
"t": 184.9,
"daa": 207,
"epoch": 3,
"class": 3,
"bps": 5000,
"nodes": [
"207/01637a3f",
"207/01637a3f",
"207/01637a3f"
]
},
{
"t": 200,
"daa": 219,
"epoch": 3,
"class": 3,
"bps": 5666,
"nodes": [
"219/0fda8440",
"219/0fda8440",
"219/0fda8440"
]
},
{
"t": 215,
"daa": 226,
"epoch": 3,
"class": 3,
"bps": 5666,
"nodes": [
"226/8ed13229",
"226/8ed13229",
"226/8ed13229"
]
},
{
"t": 230,
"daa": 236,
"epoch": 3,
"class": 3,
"bps": 5000,
"nodes": [
"236/b0be941e",
"236/b0be941e",
"236/b0be941e"
]
},
{
"t": 245,
"daa": 252,
"epoch": 4,
"class": 3,
"bps": 5666,
"nodes": [
"252/a93709a4",
"252/a93709a4",
"252/a93709a4"
]
},
{
"t": 260,
"daa": 274,
"epoch": 4,
"class": 3,
"bps": 5666,
"nodes": [
"274/099337fa",
"274/099337fa",
"274/099337fa"
]
},
{
"t": 275.1,
"daa": 282,
"epoch": 4,
"class": 3,
"bps": 5666,
"nodes": [
"282/6bd5d923",
"282/6bd5d923",
"282/6bd5d923"
]
},
{
"t": 290.1,
"daa": 296,
"epoch": 4,
"class": 3,
"bps": 5000,
"nodes": [
"296/94f37746",
"296/94f37746",
"296/94f37746"
]
},
{
"t": 305.1,
"daa": 313,
"epoch": 5,
"class": 3,
"bps": 5333,
"nodes": [
"313/b76f2f75",
"313/b76f2f75",
"313/b76f2f75"
]
},
{
"t": 320.1,
"daa": 326,
"epoch": 5,
"class": 3,
"bps": 5000,
"nodes": [
"326/9a5dee69",
"326/9a5dee69",
"326/9a5dee69"
]
},
{
"t": 335.1,
"daa": 341,
"epoch": 5,
"class": 3,
"bps": 5666,
"nodes": [
"341/058c9422",
"341/058c9422",
"341/058c9422"
]
},
{
"t": 350.2,
"daa": 360,
"epoch": 6,
"class": 3,
"bps": 5333,
"nodes": [
"360/901edc84",
"360/901edc84",
"360/901edc84"
]
},
{
"t": 365.2,
"daa": 377,
"epoch": 6,
"class": 3,
"bps": 6000,
"nodes": [
"377/e681d3ba",
"377/e681d3ba",
"377/e681d3ba"
]
},
{
"t": 380.2,
"daa": 388,
"epoch": 6,
"class": 3,
"bps": 6333,
"nodes": [
"388/05c84f63",
"388/05c84f63",
"388/05c84f63"
]
},
{
"t": 395.2,
"daa": 406,
"epoch": 6,
"class": 3,
"bps": 5666,
"nodes": [
"406/4bc6b31c",
"406/4bc6b31c",
"406/4bc6b31c"
]
},
{
"t": 410.2,
"daa": 421,
"epoch": 7,
"class": 3,
"bps": 6333,
"nodes": [
"421/6b95e961",
"421/6b95e961",
"421/6b95e961"
]
}
]
}

View file

@ -0,0 +1,511 @@
{
"pass": true,
"expect": "flip",
"signals": [
4,
4,
4
],
"checks": {
"zero_rejected_by_miners": true,
"zero_rejected_by_nodes": true,
"sinks_agree": true,
"block_counts_agree": true,
"miners_agree_on_every_program": true,
"window_line_on_every_node": true,
"every_node_signals_its_byte": true,
"chain_carries_the_bytes": true,
"template_switched_to_v4": true,
"switched_at_the_first_full_window_epoch": true,
"switched_before_the_floor": true,
"signal_line_on_every_node_same_epoch": true,
"signal_share_at_or_above_threshold": true,
"signal_line_names_seven_windows": true,
"blocks_on_both_sides": true,
"v4_ids_equal_the_cli_v4_id": true,
"v4_ids_differ_from_the_same_seed_v3_id": true
},
"window": 30,
"windows": 7,
"floor": 100000,
"v3_activation": 60,
"epoch_blocks": 60,
"lead": 10,
"first_full_window_epoch": 4,
"floor_epoch": 1667,
"node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd",
"miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner",
"template_switch": {
"epoch": 4,
"daa": 241,
"at": 228.1
},
"run_ended_at_s": 339.4,
"final_daa": 360,
"max_epoch_seen": 6,
"epochs": {
"0": {
"class": 2,
"firstSeenDaa": 0,
"at": 4.6,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"1": {
"class": 3,
"firstSeenDaa": 60,
"at": 29.7,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"2": {
"class": 3,
"firstSeenDaa": 120,
"at": 106.8,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"3": {
"class": 3,
"firstSeenDaa": 180,
"at": 166,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"4": {
"class": 4,
"firstSeenDaa": 241,
"at": 228.1,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 10000,
"signal_epoch": 4
},
"5": {
"class": 4,
"firstSeenDaa": 301,
"at": 284.2,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 10000,
"signal_epoch": null
},
"6": {
"class": 4,
"firstSeenDaa": 360,
"at": 339.4,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 10000,
"signal_epoch": null
}
},
"blocks": {
"total": 365,
"before_boundary": 241,
"after_boundary": 124,
"version_bytes": {
"0": 1,
"4": 364
},
"signal_share_bps_on_chain": 9973
},
"programs": [
{
"epoch": 0,
"class": "v2",
"program_id": "8f8806638d59850f",
"seed": "234e082d653dc69d",
"miners": 3,
"disagree": false
},
{
"epoch": 1,
"class": "v3",
"program_id": "8b03d07a1875fe62",
"seed": "08625fc57bb161d4",
"miners": 3,
"disagree": false
},
{
"epoch": 2,
"class": "v3",
"program_id": "6896042b76dabe5a",
"seed": "f74f60ade435880a",
"miners": 3,
"disagree": false
},
{
"epoch": 3,
"class": "v3",
"program_id": "26f4071960d8aeef",
"seed": "ed4e93635f53f4e2",
"miners": 3,
"disagree": false
},
{
"epoch": 4,
"class": "v4",
"program_id": "513bbe423698ac46",
"seed": "a5adea6f1fecb370",
"miners": 3,
"disagree": false
},
{
"epoch": 5,
"class": "v4",
"program_id": "ee00d3fe4a0b811c",
"seed": "706cc2b5ddd6ea3c",
"miners": 3,
"disagree": false
},
{
"epoch": 6,
"class": "v4",
"program_id": "50cbbceb1587bd20",
"seed": "cee479e6601625a5",
"miners": 3,
"disagree": false
}
],
"program_id_rows": [
{
"epoch": 4,
"seed": "a5adea6f1fecb370",
"miners_id": "513bbe423698ac46",
"miners": 3,
"cli_v3": "11695bdfc66258c9",
"cli_v4": "513bbe423698ac46"
},
{
"epoch": 5,
"seed": "706cc2b5ddd6ea3c",
"miners_id": "ee00d3fe4a0b811c",
"miners": 3,
"cli_v3": "05a7e8fbadcb906f",
"cli_v4": "ee00d3fe4a0b811c"
},
{
"epoch": 6,
"seed": "cee479e6601625a5",
"miners_id": "50cbbceb1587bd20",
"miners": 3,
"cli_v3": "2f96368ac5c87ad3",
"cli_v4": "50cbbceb1587bd20"
}
],
"accepted_per_miner": [
117,
133,
114
],
"rejected_by_miners": [
0,
0,
0
],
"rejected_by_nodes": [
0,
0,
0
],
"sinks": [
"786d8071368913fa",
"786d8071368913fa",
"786d8071368913fa"
],
"block_counts": [
364,
364,
364
],
"signal_lines": [
"Program class v4 by miner signal: epoch 4 (share 10000 bps over 30 DAA ending at seed block a5adea6f1fecb370a5a77b2c7cfd586dcb601eba8664c364c519830185159143, threshold 9500 bps in each of 7 consecutive windows, weakest 10000 bps, shares oldest first [10000, 10000, 10000, 10000, 10000, 10000, 10000], 210 of 210 blue blocks)",
"Program class v4 by miner signal: epoch 4 (share 10000 bps over 30 DAA ending at seed block a5adea6f1fecb370a5a77b2c7cfd586dcb601eba8664c364c519830185159143, threshold 9500 bps in each of 7 consecutive windows, weakest 10000 bps, shares oldest first [10000, 10000, 10000, 10000, 10000, 10000, 10000], 210 of 210 blue blocks)",
"Program class v4 by miner signal: epoch 4 (share 10000 bps over 30 DAA ending at seed block a5adea6f1fecb370a5a77b2c7cfd586dcb601eba8664c364c519830185159143, threshold 9500 bps in each of 7 consecutive windows, weakest 10000 bps, shares oldest first [10000, 10000, 10000, 10000, 10000, 10000, 10000], 210 of 210 blue blocks)"
],
"floor_lines": [
"Program class v4 from the override file: active from epoch 1667 (DAA score 100000 rounded up to the epoch boundary at 100020, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 1667 (DAA score 100000 rounded up to the epoch boundary at 100020, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 1667 (DAA score 100000 rounded up to the epoch boundary at 100020, epochs of 60 DAA)"
],
"samples": [
{
"t": 4.6,
"daa": 0,
"epoch": 0,
"class": 2,
"bps": 0,
"nodes": [
"0/234e082d",
"0/234e082d",
"0/234e082d"
]
},
{
"t": 19.7,
"daa": 45,
"epoch": 0,
"class": 2,
"bps": 0,
"nodes": [
"45/ed680eb3",
"45/ed680eb3",
"45/ed680eb3"
]
},
{
"t": 34.7,
"daa": 61,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"61/b2ac0c02",
"61/b2ac0c02",
"61/b2ac0c02"
]
},
{
"t": 49.7,
"daa": 67,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"67/cbd5a35e",
"67/cbd5a35e",
"67/cbd5a35e"
]
},
{
"t": 64.7,
"daa": 80,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"80/d88cad13",
"80/d88cad13",
"80/d88cad13"
]
},
{
"t": 79.8,
"daa": 88,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"88/83050ad0",
"88/83050ad0",
"88/83050ad0"
]
},
{
"t": 94.8,
"daa": 106,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"106/d230c5bc",
"106/d230c5bc",
"106/d230c5bc"
]
},
{
"t": 109.8,
"daa": 124,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"124/9c5750a4",
"124/9c5750a4",
"124/9c5750a4"
]
},
{
"t": 124.9,
"daa": 145,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"145/a5200bc7",
"145/a5200bc7",
"145/a5200bc7"
]
},
{
"t": 139.9,
"daa": 157,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"157/407eca8c",
"157/407eca8c",
"157/407eca8c"
]
},
{
"t": 154.9,
"daa": 172,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"172/71c56601",
"172/71c56601",
"172/71c56601"
]
},
{
"t": 170,
"daa": 181,
"epoch": 3,
"class": 3,
"bps": 0,
"nodes": [
"181/bdfe2032",
"181/bdfe2032",
"181/bdfe2032"
]
},
{
"t": 185,
"daa": 189,
"epoch": 3,
"class": 3,
"bps": 10000,
"nodes": [
"189/826a9db3",
"189/826a9db3",
"189/826a9db3"
]
},
{
"t": 200,
"daa": 207,
"epoch": 3,
"class": 3,
"bps": 10000,
"nodes": [
"207/216fd0dc",
"207/216fd0dc",
"207/216fd0dc"
]
},
{
"t": 215.1,
"daa": 221,
"epoch": 3,
"class": 3,
"bps": 10000,
"nodes": [
"221/b2e187ad",
"221/b2e187ad",
"221/b2e187ad"
]
},
{
"t": 230.1,
"daa": 241,
"epoch": 4,
"class": 4,
"bps": 10000,
"nodes": [
"241/97e1a942",
"241/97e1a942",
"241/97e1a942"
]
},
{
"t": 245.1,
"daa": 254,
"epoch": 4,
"class": 4,
"bps": 10000,
"nodes": [
"254/15744959",
"254/15744959",
"254/15744959"
]
},
{
"t": 260.2,
"daa": 270,
"epoch": 4,
"class": 4,
"bps": 10000,
"nodes": [
"270/bf72bf67",
"270/bf72bf67",
"270/bf72bf67"
]
},
{
"t": 275.2,
"daa": 287,
"epoch": 4,
"class": 4,
"bps": 10000,
"nodes": [
"287/53fb6153",
"287/53fb6153",
"287/53fb6153"
]
},
{
"t": 290.2,
"daa": 308,
"epoch": 5,
"class": 4,
"bps": 10000,
"nodes": [
"308/947c64f4",
"308/947c64f4",
"308/947c64f4"
]
},
{
"t": 305.3,
"daa": 325,
"epoch": 5,
"class": 4,
"bps": 10000,
"nodes": [
"325/526aa176",
"325/526aa176",
"325/526aa176"
]
},
{
"t": 320.3,
"daa": 336,
"epoch": 5,
"class": 4,
"bps": 10000,
"nodes": [
"336/4fcf7134",
"336/4fcf7134",
"336/4fcf7134"
]
},
{
"t": 335.3,
"daa": 354,
"epoch": 5,
"class": 4,
"bps": 10000,
"nodes": [
"354/ed96e77e",
"354/ed96e77e",
"354/ed96e77e"
]
}
]
}

View file

@ -0,0 +1,636 @@
{
"pass": true,
"expect": "floor",
"signals": [
3,
3,
3
],
"checks": {
"zero_rejected_by_miners": true,
"zero_rejected_by_nodes": true,
"sinks_agree": true,
"block_counts_agree": true,
"miners_agree_on_every_program": true,
"window_line_on_every_node": true,
"every_node_signals_its_byte": true,
"chain_carries_the_bytes": true,
"template_switched_to_v4": true,
"switched_at_the_floor_epoch": true,
"no_signal_line_on_any_node": true,
"floor_line_names_the_floor_epoch": true,
"blocks_on_both_sides": true,
"v4_ids_equal_the_cli_v4_id": true
},
"window": 30,
"windows": 7,
"floor": 360,
"v3_activation": 60,
"epoch_blocks": 60,
"lead": 10,
"first_full_window_epoch": 4,
"floor_epoch": 6,
"node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd",
"miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner",
"template_switch": {
"epoch": 6,
"daa": 362,
"at": 345.4
},
"run_ended_at_s": 461.7,
"final_daa": 480,
"max_epoch_seen": 8,
"epochs": {
"0": {
"class": 2,
"firstSeenDaa": 0,
"at": 4.6,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"1": {
"class": 3,
"firstSeenDaa": 60,
"at": 30.7,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"2": {
"class": 3,
"firstSeenDaa": 120,
"at": 95.9,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"3": {
"class": 3,
"firstSeenDaa": 180,
"at": 162,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"4": {
"class": 3,
"firstSeenDaa": 240,
"at": 223.1,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"5": {
"class": 3,
"firstSeenDaa": 300,
"at": 286.3,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"6": {
"class": 4,
"firstSeenDaa": 362,
"at": 345.4,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"7": {
"class": 4,
"firstSeenDaa": 420,
"at": 396.5,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"8": {
"class": 4,
"firstSeenDaa": 480,
"at": 461.7,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
}
},
"blocks": {
"total": 486,
"before_boundary": 361,
"after_boundary": 125,
"version_bytes": {
"0": 1,
"3": 485
},
"signal_share_bps_on_chain": 0
},
"programs": [
{
"epoch": 0,
"class": "v2",
"program_id": "8f8806638d59850f",
"seed": "234e082d653dc69d",
"miners": 3,
"disagree": false
},
{
"epoch": 1,
"class": "v3",
"program_id": "0e47ecc08def3b3c",
"seed": "87f4edf6d5390261",
"miners": 3,
"disagree": false
},
{
"epoch": 2,
"class": "v3",
"program_id": "e01d3d013e9d0fd0",
"seed": "6aa531dd57a2343a",
"miners": 3,
"disagree": false
},
{
"epoch": 3,
"class": "v3",
"program_id": "34ddb6eacf5ca8d5",
"seed": "c053141073ce26a3",
"miners": 3,
"disagree": false
},
{
"epoch": 4,
"class": "v3",
"program_id": "b03aefe32cca46b5",
"seed": "51e40d5dc38d9823",
"miners": 3,
"disagree": false
},
{
"epoch": 5,
"class": "v3",
"program_id": "3dd9cb5971851a52",
"seed": "b4adb9c80f091964",
"miners": 3,
"disagree": false
},
{
"epoch": 6,
"class": "v4",
"program_id": "aa0283cc2b5e5b0f",
"seed": "536c8ac2706c23be",
"miners": 3,
"disagree": false
},
{
"epoch": 7,
"class": "v4",
"program_id": "1d4f6d8c21386868",
"seed": "116706d2320aad69",
"miners": 3,
"disagree": false
},
{
"epoch": 8,
"class": "v4",
"program_id": "4bee3fb48487db58",
"seed": "556c3dacee5edf64",
"miners": 3,
"disagree": false
}
],
"program_id_rows": [
{
"epoch": 6,
"seed": "536c8ac2706c23be",
"miners_id": "aa0283cc2b5e5b0f",
"miners": 3,
"cli_v3": "25d2f5499f1c6060",
"cli_v4": "aa0283cc2b5e5b0f"
},
{
"epoch": 7,
"seed": "116706d2320aad69",
"miners_id": "1d4f6d8c21386868",
"miners": 3,
"cli_v3": "239d20744ba996ff",
"cli_v4": "1d4f6d8c21386868"
},
{
"epoch": 8,
"seed": "556c3dacee5edf64",
"miners_id": "4bee3fb48487db58",
"miners": 3,
"cli_v3": "a5f5e0f57f30aca7",
"cli_v4": "4bee3fb48487db58"
}
],
"accepted_per_miner": [
170,
162,
153
],
"rejected_by_miners": [
0,
0,
0
],
"rejected_by_nodes": [
0,
0,
0
],
"sinks": [
"0db7f95ee7c2d6ec",
"0db7f95ee7c2d6ec",
"0db7f95ee7c2d6ec"
],
"block_counts": [
485,
485,
485
],
"signal_lines": [
null,
null,
null
],
"floor_lines": [
"Program class v4 from the override file: active from epoch 6 (DAA score 360 rounded up to the epoch boundary at 360, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 6 (DAA score 360 rounded up to the epoch boundary at 360, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 6 (DAA score 360 rounded up to the epoch boundary at 360, epochs of 60 DAA)"
],
"samples": [
{
"t": 4.7,
"daa": 0,
"epoch": 0,
"class": 2,
"bps": 0,
"nodes": [
"0/234e082d",
"0/234e082d",
"0/234e082d"
]
},
{
"t": 19.7,
"daa": 42,
"epoch": 0,
"class": 2,
"bps": 0,
"nodes": [
"42/d1f90c89",
"42/d1f90c89",
"42/d1f90c89"
]
},
{
"t": 34.7,
"daa": 62,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"62/ecdf7c47",
"62/ecdf7c47",
"62/ecdf7c47"
]
},
{
"t": 49.8,
"daa": 69,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"69/04b8880d",
"69/04b8880d",
"69/04b8880d"
]
},
{
"t": 64.8,
"daa": 86,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"86/98eb299b",
"86/98eb299b",
"86/98eb299b"
]
},
{
"t": 79.8,
"daa": 108,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"108/dacf3726",
"108/dacf3726",
"108/dacf3726"
]
},
{
"t": 94.9,
"daa": 119,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"119/3522f373",
"119/3522f373",
"119/3522f373"
]
},
{
"t": 109.9,
"daa": 130,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"130/1cdfcc43",
"130/1cdfcc43",
"130/1cdfcc43"
]
},
{
"t": 124.9,
"daa": 150,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"150/2ed00203",
"150/2ed00203",
"150/2ed00203"
]
},
{
"t": 140,
"daa": 163,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"163/fdb35c12",
"163/fdb35c12",
"163/fdb35c12"
]
},
{
"t": 155,
"daa": 173,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"173/02d85439",
"173/02d85439",
"173/02d85439"
]
},
{
"t": 170,
"daa": 186,
"epoch": 3,
"class": 3,
"bps": 0,
"nodes": [
"186/d7afd264",
"186/d7afd264",
"186/d7afd264"
]
},
{
"t": 185,
"daa": 200,
"epoch": 3,
"class": 3,
"bps": 0,
"nodes": [
"200/3b87d7fc",
"200/3b87d7fc",
"200/3b87d7fc"
]
},
{
"t": 200.1,
"daa": 218,
"epoch": 3,
"class": 3,
"bps": 0,
"nodes": [
"218/e79e78a2",
"218/e79e78a2",
"218/e79e78a2"
]
},
{
"t": 215.1,
"daa": 236,
"epoch": 3,
"class": 3,
"bps": 0,
"nodes": [
"236/74b74819",
"236/74b74819",
"236/74b74819"
]
},
{
"t": 230.2,
"daa": 252,
"epoch": 4,
"class": 3,
"bps": 0,
"nodes": [
"252/f44062e7",
"252/f44062e7",
"252/f44062e7"
]
},
{
"t": 245.2,
"daa": 264,
"epoch": 4,
"class": 3,
"bps": 0,
"nodes": [
"264/b7f42006",
"264/b7f42006",
"264/b7f42006"
]
},
{
"t": 260.2,
"daa": 279,
"epoch": 4,
"class": 3,
"bps": 0,
"nodes": [
"279/988cb551",
"279/988cb551",
"279/988cb551"
]
},
{
"t": 275.2,
"daa": 289,
"epoch": 4,
"class": 3,
"bps": 0,
"nodes": [
"289/f38c1458",
"289/f38c1458",
"289/f38c1458"
]
},
{
"t": 290.3,
"daa": 305,
"epoch": 5,
"class": 3,
"bps": 0,
"nodes": [
"305/9125bec4",
"305/9125bec4",
"305/9125bec4"
]
},
{
"t": 305.3,
"daa": 317,
"epoch": 5,
"class": 3,
"bps": 0,
"nodes": [
"317/9dc60b29",
"317/9dc60b29",
"317/9dc60b29"
]
},
{
"t": 320.3,
"daa": 330,
"epoch": 5,
"class": 3,
"bps": 0,
"nodes": [
"330/28eaf8e8",
"330/28eaf8e8",
"330/28eaf8e8"
]
},
{
"t": 335.4,
"daa": 346,
"epoch": 5,
"class": 3,
"bps": 0,
"nodes": [
"346/2c358b1f",
"346/2c358b1f",
"346/2c358b1f"
]
},
{
"t": 350.4,
"daa": 367,
"epoch": 6,
"class": 4,
"bps": 0,
"nodes": [
"367/b420706f",
"367/b420706f",
"367/b420706f"
]
},
{
"t": 365.4,
"daa": 381,
"epoch": 6,
"class": 4,
"bps": 0,
"nodes": [
"381/9363f9bd",
"381/9363f9bd",
"381/9363f9bd"
]
},
{
"t": 380.5,
"daa": 397,
"epoch": 6,
"class": 4,
"bps": 0,
"nodes": [
"397/49b6c6a3",
"397/49b6c6a3",
"397/49b6c6a3"
]
},
{
"t": 395.5,
"daa": 417,
"epoch": 6,
"class": 4,
"bps": 0,
"nodes": [
"417/cbc74290",
"417/cbc74290",
"417/cbc74290"
]
},
{
"t": 410.5,
"daa": 430,
"epoch": 7,
"class": 4,
"bps": 0,
"nodes": [
"430/08e73746",
"430/08e73746",
"430/08e73746"
]
},
{
"t": 425.6,
"daa": 449,
"epoch": 7,
"class": 4,
"bps": 0,
"nodes": [
"449/6067e6b4",
"449/6067e6b4",
"449/6067e6b4"
]
},
{
"t": 440.6,
"daa": 460,
"epoch": 7,
"class": 4,
"bps": 0,
"nodes": [
"460/1ae21462",
"460/1ae21462",
"460/1ae21462"
]
},
{
"t": 455.6,
"daa": 468,
"epoch": 7,
"class": 4,
"bps": 0,
"nodes": [
"468/f9f3ca09",
"468/f9f3ca09",
"468/f9f3ca09"
]
}
]
}

View file

@ -0,0 +1,555 @@
{
"pass": true,
"expect": "no-flip",
"signals": [
4,
4,
3
],
"checks": {
"zero_rejected_by_miners": true,
"zero_rejected_by_nodes": true,
"sinks_agree": true,
"block_counts_agree": true,
"miners_agree_on_every_program": true,
"window_line_on_every_node": true,
"every_node_signals_its_byte": true,
"chain_carries_the_bytes": true,
"template_never_v4": true,
"no_signal_line_on_any_node": true,
"ran_the_epochs": true,
"v3_programs_seen": true,
"signal_share_under_threshold_on_chain": true
},
"window": 30,
"windows": 7,
"floor": 100000,
"v3_activation": 60,
"epoch_blocks": 60,
"lead": 10,
"first_full_window_epoch": 4,
"floor_epoch": 1667,
"node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd",
"miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner",
"template_switch": null,
"run_ended_at_s": 414.3,
"final_daa": 420,
"max_epoch_seen": 7,
"epochs": {
"0": {
"class": 2,
"firstSeenDaa": 0,
"at": 4.6,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"1": {
"class": 3,
"firstSeenDaa": 60,
"at": 27.7,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"2": {
"class": 3,
"firstSeenDaa": 120,
"at": 107.8,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"3": {
"class": 3,
"firstSeenDaa": 182,
"at": 159.9,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 0,
"signal_epoch": null
},
"4": {
"class": 3,
"firstSeenDaa": 240,
"at": 226,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 6333,
"signal_epoch": null
},
"5": {
"class": 3,
"firstSeenDaa": 300,
"at": 286.1,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 6000,
"signal_epoch": null
},
"6": {
"class": 3,
"firstSeenDaa": 361,
"at": 350.2,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 5666,
"signal_epoch": null
},
"7": {
"class": 3,
"firstSeenDaa": 420,
"at": 414.3,
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
"bps": 6000,
"signal_epoch": null
}
},
"blocks": {
"total": 426,
"before_boundary": 426,
"after_boundary": 0,
"version_bytes": {
"0": 1,
"3": 129,
"4": 296
},
"signal_share_bps_on_chain": 6948
},
"programs": [
{
"epoch": 0,
"class": "v2",
"program_id": "8f8806638d59850f",
"seed": "234e082d653dc69d",
"miners": 3,
"disagree": false
},
{
"epoch": 1,
"class": "v3",
"program_id": "769b4dd980c7f126",
"seed": "fba8aa7ae82c47e9",
"miners": 3,
"disagree": false
},
{
"epoch": 2,
"class": "v3",
"program_id": "56335da09deca248",
"seed": "64b63eff660aaeec",
"miners": 3,
"disagree": false
},
{
"epoch": 3,
"class": "v3",
"program_id": "f2f883996849c0e5",
"seed": "b0f742cf4d197b15",
"miners": 3,
"disagree": false
},
{
"epoch": 4,
"class": "v3",
"program_id": "c59d7d33e4749671",
"seed": "4bec73e616207ebc",
"miners": 3,
"disagree": false
},
{
"epoch": 5,
"class": "v3",
"program_id": "8bab4d2320643cc8",
"seed": "bf8b384490f61b1a",
"miners": 3,
"disagree": false
},
{
"epoch": 6,
"class": "v3",
"program_id": "42a5cc579d9dc917",
"seed": "9399732456e83970",
"miners": 3,
"disagree": false
},
{
"epoch": 7,
"class": "v3",
"program_id": "7d7f6dfb6df1b584",
"seed": "c47f438d5f83e60e",
"miners": 3,
"disagree": false
}
],
"program_id_rows": [],
"accepted_per_miner": [
155,
141,
129
],
"rejected_by_miners": [
0,
0,
0
],
"rejected_by_nodes": [
0,
0,
0
],
"sinks": [
"5d0d7f21de33c29b",
"5d0d7f21de33c29b",
"5d0d7f21de33c29b"
],
"block_counts": [
425,
425,
425
],
"signal_lines": [
null,
null,
null
],
"floor_lines": [
"Program class v4 from the override file: active from epoch 1667 (DAA score 100000 rounded up to the epoch boundary at 100020, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 1667 (DAA score 100000 rounded up to the epoch boundary at 100020, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 1667 (DAA score 100000 rounded up to the epoch boundary at 100020, epochs of 60 DAA)"
],
"samples": [
{
"t": 4.6,
"daa": 0,
"epoch": 0,
"class": 2,
"bps": 0,
"nodes": [
"0/234e082d",
"0/234e082d",
"0/234e082d"
]
},
{
"t": 19.7,
"daa": 42,
"epoch": 0,
"class": 2,
"bps": 0,
"nodes": [
"42/b78cd510",
"42/b78cd510",
"42/b78cd510"
]
},
{
"t": 34.7,
"daa": 66,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"66/e6e0e6b6",
"66/e6e0e6b6",
"66/e6e0e6b6"
]
},
{
"t": 49.7,
"daa": 69,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"69/3e334b41",
"69/3e334b41",
"69/3e334b41"
]
},
{
"t": 64.7,
"daa": 74,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"74/f6732168",
"74/f6732168",
"74/f6732168"
]
},
{
"t": 79.8,
"daa": 91,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"91/4c5172a4",
"91/4c5172a4",
"91/4c5172a4"
]
},
{
"t": 94.8,
"daa": 104,
"epoch": 1,
"class": 3,
"bps": 0,
"nodes": [
"104/8bed6da0",
"104/8bed6da0",
"104/8bed6da0"
]
},
{
"t": 109.8,
"daa": 123,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"123/7c7f2a35",
"123/7c7f2a35",
"123/7c7f2a35"
]
},
{
"t": 124.8,
"daa": 134,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"134/11ee9640",
"134/11ee9640",
"134/11ee9640"
]
},
{
"t": 139.9,
"daa": 161,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"161/615ad11d",
"161/615ad11d",
"161/615ad11d"
]
},
{
"t": 154.9,
"daa": 172,
"epoch": 2,
"class": 3,
"bps": 0,
"nodes": [
"172/52e6efa7",
"172/52e6efa7",
"172/52e6efa7"
]
},
{
"t": 169.9,
"daa": 195,
"epoch": 3,
"class": 3,
"bps": 5666,
"nodes": [
"195/d5003862",
"195/d5003862",
"195/d5003862"
]
},
{
"t": 184.9,
"daa": 204,
"epoch": 3,
"class": 3,
"bps": 5666,
"nodes": [
"204/f43a9613",
"204/f43a9613",
"204/f43a9613"
]
},
{
"t": 199.9,
"daa": 217,
"epoch": 3,
"class": 3,
"bps": 6333,
"nodes": [
"217/f2899339",
"217/f2899339",
"217/f2899339"
]
},
{
"t": 215,
"daa": 229,
"epoch": 3,
"class": 3,
"bps": 5333,
"nodes": [
"229/aa39dc5b",
"229/aa39dc5b",
"229/aa39dc5b"
]
},
{
"t": 230,
"daa": 243,
"epoch": 4,
"class": 3,
"bps": 6333,
"nodes": [
"243/84160223",
"243/84160223",
"243/84160223"
]
},
{
"t": 245,
"daa": 256,
"epoch": 4,
"class": 3,
"bps": 5333,
"nodes": [
"256/95be3208",
"256/95be3208",
"256/95be3208"
]
},
{
"t": 260,
"daa": 274,
"epoch": 4,
"class": 3,
"bps": 6000,
"nodes": [
"274/17786bd2",
"274/17786bd2",
"274/17786bd2"
]
},
{
"t": 275.1,
"daa": 287,
"epoch": 4,
"class": 3,
"bps": 5666,
"nodes": [
"287/5f1d09c0",
"287/5f1d09c0",
"287/5f1d09c0"
]
},
{
"t": 290.1,
"daa": 304,
"epoch": 5,
"class": 3,
"bps": 6000,
"nodes": [
"304/b3b30ae6",
"304/b3b30ae6",
"304/b3b30ae6"
]
},
{
"t": 305.1,
"daa": 318,
"epoch": 5,
"class": 3,
"bps": 5666,
"nodes": [
"318/f040c6e4",
"318/f040c6e4",
"318/f040c6e4"
]
},
{
"t": 320.1,
"daa": 341,
"epoch": 5,
"class": 3,
"bps": 6333,
"nodes": [
"341/34fddad1",
"341/34fddad1",
"341/34fddad1"
]
},
{
"t": 335.2,
"daa": 350,
"epoch": 5,
"class": 3,
"bps": 5333,
"nodes": [
"350/93997324",
"350/93997324",
"350/93997324"
]
},
{
"t": 350.2,
"daa": 361,
"epoch": 6,
"class": 3,
"bps": 5666,
"nodes": [
"361/a6f64b19",
"361/a6f64b19",
"361/a6f64b19"
]
},
{
"t": 365.2,
"daa": 381,
"epoch": 6,
"class": 3,
"bps": 5666,
"nodes": [
"381/c5f7f5e5",
"381/c5f7f5e5",
"381/c5f7f5e5"
]
},
{
"t": 380.2,
"daa": 395,
"epoch": 6,
"class": 3,
"bps": 6000,
"nodes": [
"395/671be986",
"395/671be986",
"395/671be986"
]
},
{
"t": 395.3,
"daa": 404,
"epoch": 6,
"class": 3,
"bps": 5666,
"nodes": [
"404/e5f584eb",
"404/e5f584eb",
"404/e5f584eb"
]
},
{
"t": 410.3,
"daa": 417,
"epoch": 6,
"class": 3,
"bps": 6000,
"nodes": [
"417/ccbc9572",
"417/ccbc9572",
"417/ccbc9572"
]
}
]
}

View file

@ -98,7 +98,7 @@ Status: PROPOSED spec text for spec 01 (a new section 1.12.2 beside the epoch ru
1. **The carrier: the block header's `version` field, 2 bytes little-endian.** The low byte is the block version as before (2 on every Igneum network; `block_version_of`). The high byte is the producer's OBJECT VERSION, the highest program class the node that built the template runs: 4 for this binary (`CLASS_SIGNAL_V4`), 0 on every block made before it (`signalled_version(2, 4) = 0x0402`; `class_signal_of`). Why the header and not the coinbase extra data: the tally is read in header validation and by a node that holds headers only (a pruning-proof sync, a headers-first IBD), the header is what GHOSTDAG orders and what the finality rule already reads for its weight (the vote key hash is a header field, `finality.rs`), and the bytes are already there: no new field, no new hash preimage, no proto change for the header. A node before this binary rejects any header whose version is not exactly 2 (`check_header_version`), which is why the signalling binary ships in the same one-sweep rollout as the digest flip it already needs (section 2); from this binary on, only the low byte is checked, so a later object (5, 6) can be signalled to it without another header rule.
2. **The weight: blue blocks, the finality rule's convention.** For epoch `e` the anchor is its seed block `S_e`, the last selected-chain block whose DAA score is below `L e - lead` (the block the epoch seed is already taken from, `HeaderProcessor::epoch_seed`; so an epoch's seed and its class are decided at the same block of the same chain). The window is the `W` DAA below and including `S_e`: the selected chain is walked down from `S_e`, every chain block's mergeset blues counted once (the `compute_weights` walk of `finality.rs`, bounded by the merge depth), a blue block counted when its DAA score lies in `(daa(S_e) - W, daa(S_e)]`, and signalling when its object byte is at least 4. Share = signalling / total, in basis points.
3. **The decision, per epoch, monotone.** Epoch `e` is class v4 when (a) `L e >= N6`, the floor (the fixed height, rounded up to the epoch boundary exactly as the v3 switch is: `program_class_for_epoch_at`), or (b) epoch `e - 1` was v4, or (c) the window ending at `S_e` is FULL (`daa(S_e) >= W`) and its share is at least 9,500 bps. A signal moves the class one step: the rule answers v4 only where the floor rule answers v3 (below the v3 switch the answer is v2 whatever is signalled). The decision is memoised per seed block, so a fork of the chain has its own entries and one tally is paid per epoch per process. The epoch-boundary rounding of the v2 to v3 switch is unchanged: a class never changes inside an epoch, every block's class is its epoch's.
4. **The window `W`: 86,400 DAA (one day of blocks at 1 block/s), `program_class_v4_signal_window_daa` in the override file, in the digest right after the floor; 0 = signalling off, the floor alone (2.0's rule, byte for byte).** Why a day: the share must mean "the fleet that mines, not the fleet that happens to be up this hour", and a day covers every box's daily pattern (the rented boxes come and go by the hour); it is long enough that 95 percent cannot be reached by a burst and short enough that the flip lands within a day of the last upgrade; the finality rule's 30-day window answers a different question (who may vote) and would hold the class for a month after the fleet was ready. On the fast-time profile `W` is 120 (two epochs of 60).
4. **The window `W`: 86,400 DAA (one day of blocks at 1 block/s), `program_class_v4_signal_window_daa` in the override file, in the digest once set (the 0.3.15 rule), right after the floor; 0 = signalling off, the floor alone (2.0's rule, byte for byte). REVISED 0.3.16 (Horizon consensus-security): the threshold must hold in EACH of `CLASS_SIGNAL_WINDOWS` = 7 CONSECUTIVE windows ending at the epoch's seed block (a week of days), because one day of 95 percent can be bought for a day (about 19 N of hash for 24 hours) and would force a flip onto a fleet not yet on the object, and seven consecutive days cannot be bought unnoticed; the floor is then set a week past the publish (DAA + 604,800 rounded up), not 14,400. The tally is one walk of `7 W` DAA bucketed per window; the decision rests on the weakest of the seven (`programClassV4SignalWeakestBps`), the console shows the newest (`programClassV4SignalBps`).** Why a day: the share must mean "the fleet that mines, not the fleet that happens to be up this hour", and a day covers every box's daily pattern (the rented boxes come and go by the hour); it is long enough that 95 percent cannot be reached by a burst and short enough that the flip lands within a day of the last upgrade; the finality rule's 30-day window answers a different question (who may vote) and would hold the class for a month after the fleet was ready. On the fast-time profile `W` is 120 (two epochs of 60).
5. **The threshold: 9,500 bps (95 percent), a constant (`CLASS_SIGNAL_THRESHOLD_BPS`), not a file field**, so no file can lower it; 95 percent of the blue blocks of a day is 95 percent of the hash rate of that day, the coordinator's figure; a box that cannot mine v4 (an old worker, section 2's wire note) is at most 5 percent of the hash rate at the flip, and the floor catches the rest.
6. **The floor `N6`: `program_class_v4_activation_daa`, set at the publish as DAA + 14,400 rounded up to the epoch boundary (the 2.0 rule for N4), checked `N6 - DAA >= 10,800`.** A stalled signal (a fleet that never reaches 95 percent) cannot hold the class forever: at `N6` v4 holds regardless. `never` is allowed in the file and means no floor (the signal alone decides; not for the devnet publish).
7. **What a node reports.** `PowEpochInfo` and the template's `powEpoch` carry `programClassV4SignalWindowDaa`, `programClassV4SignalBps` (the share of the window ending at the SINK, the live tally the next decision is heading for), `programClassSignal` (this node's byte) and `programClassV4SignalEpoch` (the epoch v4 was decided by signal on this chain, when it has been); gRPC fields 20 to 23. The daemon prints `Program class v4 signal window from the override file: W DAA ending at each epoch's seed block, threshold 9500 bps of blue blocks; the fixed height is the floor` beside the floor line, and `Program class v4 by miner signal: epoch E (share X bps over W DAA ending at seed block S, threshold 9500 bps, N of M blue blocks)` once per flip.
@ -129,4 +129,15 @@ Two fields join the live object: `program_class_v4_activation_daa` is now the FL
The first pass of the three good cases (17:01 to 17:20Z) had the same chain behaviour and failed on one harness check of its own (`chain_carries_the_bytes` demanded every byte on the chain be a node's and genesis carries byte 0); the check was corrected to allow the one genesis block and the three cases re-run above. Every number here is a count, a line or an id; the Mac's load average (about 9, other agents' builds) moves the wall times only.
### 6.5 Seven windows (0.3.16, fork 0760b844): the fast-time gate re-run
The rule changed on the 0.3.16 branch (Horizon consensus-security): 95 percent in EACH of `CLASS_SIGNAL_WINDOWS = 7` consecutive windows ending at the epoch's seed block, decided on the weakest of the seven; one day of 95 percent can be bought for a day, seven cannot unnoticed. With the harness's 30-DAA window (two per 60-DAA epoch) the seven windows are 210 DAA and the first epoch whose seed block (at 60 e - 10) has 210 DAA below it is epoch 4 (seed at DAA 230), so a flip lands at DAA 240. A floor is REQUIRED for a signalling case since the 0.3.15 canary rule (a node stamps and reads the signal only with both v4 fields set); the first 0.3.16 pass of this harness (20:56Z) ran with the floor at never and every chain byte read 0. The four cases below ran with `--floor 100000` (27 hours of fast-time chain away) or the floor under test, binaries from the fork at 0760b844 (`target-ca3v4`), 6 October 2026, load average 7 to 10 (other agents' builds on the box; counts and ids only here).
| Case | Run (UTC) | SUMMARY | Summary file |
|---|---|---|---|
| Two of three signal (`--signal 4,4,3 --floor 100000 --expect no-flip --epochs 7`) | 20:57:5x to 21:04:45 | PASS: no v4 epoch over epochs 0 to 7; the weakest-window share at the sink 5,666 to 6,333 bps from epoch 4 (0 before: fewer than seven windows exist); on the chain 296 blocks byte 4, 129 byte 3, genesis byte 0 (6,948 bps); no signal line on any node; 0 rejected; one sink `5d0d7f21` at 425/425/425 | `counter-asic-3-gate/class-v4-signal7-no-flip.json` |
| All three signal (`--signal 4,4,4 --floor 100000 --expect flip`) | 21:04:5x to 21:10:29 | PASS: the share 10,000 bps in every window; the template flipped at epoch 4 (DAA 241 seen, boundary 240), the first epoch with seven full windows, before the floor; 3 of 3 nodes printed `Program class v4 by miner signal: epoch 4 (share 10000 bps over 30 DAA ending at seed block a5adea6f..., threshold 9500 bps in each of 7 consecutive windows, weakest 10000 bps, shares oldest first [10000 x 7], 210 of 210 blue blocks)`; 241 / 124 blocks either side; 0 rejected; one sink `786d8071` at 364/364/364; the id assertion on epochs 4, 5, 6 (miners' id = CLI v4 id, differs from CLI v3 id) | `class-v4-signal7-flip.json` |
| Nobody signals, the floor at 360 (`--signal 3,3,3 --floor 360 --expect floor`) | 21:10:3x to 21:18:15 | PASS: share 0 bps in every window (485 blocks byte 3); no signal line; the class flipped at epoch 6 (DAA 362 seen, the floor's boundary 360) and not before; `active from epoch 6` on 3 of 3; 361 / 125 blocks; 0 rejected; one sink `0db7f95e` at 485/485/485; the id assertion on epochs 6, 7, 8 | `class-v4-signal7-floor.json` |
| The known-failed case (`--signal 4,4,3 --floor 100000 --expect flip --epochs 7`) | 21:18:17 to 21:25:10 | FAIL as it must: no flip at a weakest-window share of 5,333 to 6,333 bps (two CPU miners of three signalling; 288 blocks byte 4, 135 byte 3 on the chain, 6,792 bps); the harness reports `FAILED CHECK` on nine checks (`template_switched_to_v4`, `switched_at_the_first_full_window_epoch`, `switched_before_the_floor`, `signal_line_on_every_node_same_epoch`, `signal_share_at_or_above_threshold`, `signal_line_names_seven_windows`, `blocks_on_both_sides`, `v4_ids_equal_the_cli_v4_id`, `v4_ids_differ_from_the_same_seed_v3_id`) and exits 1; one sink `1c1eae6d` at 423/423/423 | `class-v4-signal7-failed-case.json` |
Fork suites on the signalling change (0562a7f2): kaspa-consensus-core 110 + 7 (the signal-rule test, the window in the params, digest and fast-time file tests; the pinned devnet digest re-pinned from `3c505021...` to `7f2e49beabc253f327c5ac6bb457a674ea7f527af2971c95d3bdf65ef8bcf977`), kaspa-consensus lib 98 (the template test now reads the low byte and the signal byte), kaspa-pow with the feature 15; PC 2: section G6 of `node-gates.md`.

View file

@ -4,17 +4,24 @@
// so the class v4 decision is made by the miners' blue-block share over the signal window, not by a fixed height.
// Ports 29690 and up, network igneum-devnet-969, data /tmp/igneum-fast-time-v4s; the class-v4.mjs shape otherwise:
// override-60x.json with CPU genesis bits, v3 from --v3-activation (default 60, epoch 1), the v4 floor at --floor
// (default never), the window at --window (default 120 DAA, two epochs; the first epoch whose seed block has DAA >= 120
// is epoch 3, seed at DAA 169), one real CPU miner per node.
// (default never), the window at --window (default 30 DAA), one real CPU miner per node.
//
// The rule (0.3.16, Horizon consensus-security): 95 percent in EACH of CLASS_SIGNAL_WINDOWS = 7 consecutive windows
// of --window DAA ending at the epoch's seed block (one day of 95 percent can be bought for a day; seven cannot
// unnoticed). With the default 30-DAA window (two per 60-DAA epoch) the seven windows are 210 DAA, and the first
// epoch whose seed block (at 60 e - 10) has 210 DAA below it is epoch 4 (seed at DAA 230), so the flip is at DAA 240.
//
// The three cases and the known-failed case:
// --signal 4,4,3 --expect no-flip two of three miners signal: 67 percent, the class must stay v3 (run 7 epochs)
// --signal 4,4,4 --expect flip all three: it flips at epoch 3, the first boundary with a full window
// --signal 3,3,3 --floor 300 --expect floor nobody signals: it flips at the floor (epoch 5, DAA 300) and not before
// --signal 4,4,3 --expect flip the known-failed case: the harness must report FAIL (no flip happened)
// --signal 4,4,3 --floor 100000 --expect no-flip two of three miners signal: 67 percent, the class must stay v3 (run 7 epochs)
// --signal 4,4,4 --floor 100000 --expect flip all three: it flips at epoch 4, the first boundary with seven full windows
// --signal 3,3,3 --floor 360 --expect floor nobody signals: it flips at the floor (epoch 6, DAA 360) and not before
// --signal 4,4,3 --floor 100000 --expect flip the known-failed case: the harness must report FAIL (no flip happened)
// The floor is REQUIRED for a signalling case: since the 0.3.15 canary rule a node stamps and reads the signal only with
// both v4 fields set (the window and a floor), so a floor at never means nobody signals (the first 0.3.16 run of this
// harness, 20:56Z: chain bytes all 0); a far floor (100,000 DAA, 27 hours of fast-time chain) keeps it out of the run.
//
// node infra/fast-time/class-v4-signal.mjs --signal a,b,c --expect flip|no-flip|floor [--floor <daa>|never]
// [--window 120] [--v3-activation 60] [--secs 480] [--epochs 7]
// [--window 30] [--v3-activation 60] [--secs 480] [--epochs 7]
// IGNEUMD, IGNEUM_MINER, IGNEUM_POW name the binaries (defaults: the ca3 fork worktree's target-ca3v4/release and
// igneum-pow/target/release/igneum-pow).
@ -41,7 +48,8 @@ const SECS = flag('secs', 480);
const EPOCHS = flag('epochs', 7);
const FLOOR = actflag('floor', null);
const V3_ACTIVATION = actflag('v3-activation', 60);
const WINDOW = flag('window', 120);
const WINDOW = flag('window', 30);
const WINDOWS = 7; // CLASS_SIGNAL_WINDOWS in the node
const SIGNAL = (sflag('signal') || '4,4,4').split(',').map(Number);
const EXPECT = sflag('expect') || 'flip';
if (!['flip', 'no-flip', 'floor'].includes(EXPECT) || SIGNAL.length !== 3) { console.error('usage: --signal a,b,c --expect flip|no-flip|floor'); process.exit(2); }
@ -58,9 +66,9 @@ const LEAD = field('pow_epoch_lead');
const DAY_MS = field('pow_day_ms');
const FIRST_V3_EPOCH = V3_ACTIVATION == null ? null : Math.ceil(V3_ACTIVATION / EPOCH);
const FLOOR_EPOCH = FLOOR == null ? null : Math.ceil(FLOOR / EPOCH);
// the first epoch whose seed block (the last chain block below L*e - lead) can have DAA >= WINDOW: L*e - lead - 1 >= WINDOW
// the first epoch whose seed block (the last chain block below L*e - lead) has WINDOWS x WINDOW DAA below it
let FIRST_FULL_EPOCH = 0;
while (FIRST_FULL_EPOCH * EPOCH - LEAD - 1 < WINDOW) FIRST_FULL_EPOCH++;
while (FIRST_FULL_EPOCH * EPOCH - LEAD - 1 < WINDOWS * WINDOW) FIRST_FULL_EPOCH++;
export function mergeOverrideText(text, fields) {
let out = text;
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
@ -70,7 +78,7 @@ export function mergeOverrideText(text, fields) {
const asText = (v) => v == null ? NEVER : String(v);
const override = `${TMP}/override.json`;
writeFileSync(override, mergeOverrideText(baseText, { genesis_bits: GENESIS_BITS, skip_proof_of_work: false, program_class_v3_activation_daa: asText(V3_ACTIVATION), program_class_v4_activation_daa: asText(FLOOR), program_class_v4_signal_window_daa: String(WINDOW) }));
log(`signals ${SIGNAL.join('/')}, expect ${EXPECT}; v3 from ${V3_ACTIVATION ?? 'never'} (epoch ${FIRST_V3_EPOCH ?? 'none'}), v4 floor ${FLOOR ?? 'never'} (epoch ${FLOOR_EPOCH ?? 'none'}), window ${WINDOW} DAA (the first epoch with a full window is ${FIRST_FULL_EPOCH}); ${EPOCH} DAA per epoch, lead ${LEAD}; run ${SECS} s or ${EPOCHS} epochs`);
log(`signals ${SIGNAL.join('/')}, expect ${EXPECT}; v3 from ${V3_ACTIVATION ?? 'never'} (epoch ${FIRST_V3_EPOCH ?? 'none'}), v4 floor ${FLOOR ?? 'never'} (epoch ${FLOOR_EPOCH ?? 'none'}), window ${WINDOW} DAA x ${WINDOWS} consecutive (the first epoch with seven full windows is ${FIRST_FULL_EPOCH}); ${EPOCH} DAA per epoch, lead ${LEAD}; run ${SECS} s or ${EPOCHS} epochs`);
class Node {
constructor(i, connect = []) {
@ -109,7 +117,7 @@ async function stopAll() {
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
const minerLog = (i) => { try { return readFileSync(`${TMP}/cpu${i}.log`, 'utf8').split('\n'); } catch { return []; } };
const SIGNAL_LINE = /Program class v4 by miner signal: epoch (\d+) \(share (\d+) bps/;
const SIGNAL_LINE = /Program class v4 by miner signal: epoch (\d+) \(share (\d+) bps over \d+ DAA ending at seed block \S+, threshold \d+ bps in each of (\d+) consecutive windows, weakest (\d+) bps/;
const FLOOR_LINE = /Program class v4 from the override file/;
const WINDOW_LINE = /Program class v4 signal window from the override file/;
const OWN_LINE = /Program class signal from IGNEUM_CLASS_SIGNAL: this node signals object version (\d+)/;
@ -135,7 +143,7 @@ while (Date.now() - t0 < SECS * 1000) {
const t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
const pe = t.powEpoch || t.pow_epoch || {};
daa = pe.virtualDaaScore ?? t.block?.header?.daaScore; epoch = pe.epochIndex; cls = pe.programClass; nextCls = pe.nextProgramClass;
eraSeed = pe.eraSeed; bps = pe.programClassV4SignalBps; win = pe.programClassV4SignalWindowDaa; sig = pe.programClassSignal; sigEpoch = pe.programClassV4SignalEpoch;
eraSeed = pe.eraSeed; bps = pe.programClassV4SignalWeakestBps ?? pe.programClassV4SignalBps; win = pe.programClassV4SignalWindowDaa; sig = pe.programClassSignal; sigEpoch = pe.programClassV4SignalEpoch;
} catch (e) { log(`template: ${e.message}`); }
if (epoch != null && epoch !== lastEpoch) {
epochs.set(epoch, { class: cls, firstSeenDaa: daa, at: +since(), eraSeed: eraSeed == null ? null : String(eraSeed), bps, signal_epoch: sigEpoch ?? null });
@ -204,7 +212,8 @@ const rejectedMiner = [0, 1, 2].map(i => minerLog(i).filter(l => /rejected nonce
const rejectedNode = nodes.map(n => n.grepLog(/PoW rejected|Rejected block|rejected block/i));
const signalLines = nodes.map(n => n.grepLog(SIGNAL_LINE).map(l => l.replace(/^.*?(Program class v4 by miner signal)/, '$1'))[0] || null);
const signalEpochs = signalLines.map(l => { const m = l && SIGNAL_LINE.exec(l); return m ? +m[1] : null; });
const signalShares = signalLines.map(l => { const m = l && SIGNAL_LINE.exec(l); return m ? +m[2] : null; });
const signalShares = signalLines.map(l => { const m = l && SIGNAL_LINE.exec(l); return m ? +m[4] : null; }); // the weakest of the seven
const signalWindows = signalLines.map(l => { const m = l && SIGNAL_LINE.exec(l); return m ? +m[3] : null; });
const floorLines = nodes.map(n => n.grepLog(FLOOR_LINE).map(l => l.replace(/^.*?(Program class v4 from)/, '$1'))[0] || null);
const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
const counts = dag.map(d => d.blockCount ?? '?');
@ -231,6 +240,7 @@ if (EXPECT === 'flip') {
switched_before_the_floor: firstV4 != null && (FLOOR_EPOCH == null || firstV4.epoch < FLOOR_EPOCH),
signal_line_on_every_node_same_epoch: signalEpochs.every(e => e != null) && new Set(signalEpochs).size === 1 && signalEpochs[0] === (firstV4 && firstV4.epoch),
signal_share_at_or_above_threshold: signalShares.every(s => s != null && s >= 9500),
signal_line_names_seven_windows: signalWindows.every(w => w === WINDOWS),
blocks_on_both_sides: before.length > 0 && after.length > 0,
v4_ids_equal_the_cli_v4_id: idRows.length > 0 && idRows.every(r => r.cli_v4 != null && r.cli_v4 === r.miners_id && r.miners === 3),
v4_ids_differ_from_the_same_seed_v3_id: idRows.length > 0 && idRows.every(r => r.cli_v3 != null && r.cli_v3 !== r.miners_id),
@ -257,7 +267,7 @@ if (EXPECT === 'flip') {
}
const pass = Object.values(checks).every(Boolean);
const summary = {
pass, expect: EXPECT, signals: SIGNAL, checks, window: WINDOW, floor: FLOOR ?? 'never', v3_activation: V3_ACTIVATION ?? 'never', epoch_blocks: EPOCH, lead: LEAD, first_full_window_epoch: FIRST_FULL_EPOCH, floor_epoch: FLOOR_EPOCH,
pass, expect: EXPECT, signals: SIGNAL, checks, window: WINDOW, windows: WINDOWS, floor: FLOOR ?? 'never', v3_activation: V3_ACTIVATION ?? 'never', epoch_blocks: EPOCH, lead: LEAD, first_full_window_epoch: FIRST_FULL_EPOCH, floor_epoch: FLOOR_EPOCH,
node: IGNEUMD, miner: CPU_MINER, template_switch: firstV4, run_ended_at_s: endAt, final_daa: lastDaa, max_epoch_seen: maxEpochSeen,
epochs: Object.fromEntries([...epochs.entries()].map(([k, v]) => [k, v])),
blocks: { total: blocks.length, before_boundary: before.length, after_boundary: after.length, version_bytes: versionBytes, signal_share_bps_on_chain: signalShareOnChain },

View file

@ -0,0 +1,279 @@
#!/usr/bin/env node
// Horizon lane 5, proposal 2 (docs/analysis/horizon/network.md 5.4 and 6): the difficulty controller against a
// relay-delayed DAG. Three nodes in a star, the two spokes reaching the hub through a TCP proxy that holds every byte
// for --delay-ms in each direction (the handshake's first --handshake-ms pass undelayed: the p2p version exchange
// times out at 4 s, protocol/p2p/src/handshake.rs), one real CPU miner per node, override-60x.json re-rated to --bps.
// A block crosses a link in three legs (inv, request, block: Kaspa's relay flow), so the document's propagation
// delay d (--propagation-ms, 3,000 or 30,000) is one third per leg: --delay-ms defaults to --propagation-ms / 3.
//
// The claim under test: under rule v3 (difficulty_v3_activation_daa 0: every chain step counts every merged block's
// work over the real span, the clock cap and lag bound in DAA seconds, m x 20 s per merging step) the DAG rate holds
// the target and the difficulty tracks the miners' hash within 10 percent after --measure-from seconds; under the
// live rule (v2: blue work over a 20 T capped step) a 3-s delay at 10 bps settles near 6.67 blocks/s with the
// difficulty 1.5x the hash-implied value (controller.py), which is this harness's known-failed case.
//
// The cases (each --secs 900, measured from --measure-from 600):
// --bps 10 --propagation-ms 3000 --rule v3 --expect pass the proposal's first case
// --bps 1 --propagation-ms 30000 --rule v3 --expect pass the proposal's second case
// --bps 10 --propagation-ms 3000 --rule v2 --expect fail the known-failed case: the harness must report FAIL
//
// Pass = |DAG rate / target - 1| <= 0.10 over the measured window AND |expected hashes per block / (hash x T) - 1|
// <= 0.10 averaged over the window, where expected hashes per block come from the sink's bits (2^256 / (target + 1))
// and hash is the sum of the three miners' own `hash=... MH/s` status lines (every 30 s). The red share, the chain
// step spacing and the three nodes' block counts are reported, not judged.
//
// node infra/fast-time/difficulty-v3.mjs --bps 10 --propagation-ms 3000 --rule v3 --expect pass [--secs 900] [--delay-ms <per leg>]
// [--measure-from 600] [--genesis-bits 0x1f080000] [--handshake-ms 5000] [--out docs/plans/counter-asic-3-gate/difficulty-v3-<case>.json]
// IGNEUMD, IGNEUM_MINER name the binaries (defaults: vendor/igneum-node-0316/target-0316/release).
import { spawn } from 'node:child_process';
import { createServer, connect as netConnect } from 'node:net';
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
import { devAddress } from '../../tools/harness/lib/address.mjs';
const ROOT = new URL('../../', import.meta.url).pathname;
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
const BIN = process.env.IGNEUM_0316_BIN || `${ROOT}vendor/igneum-node-0316/target-0316/release`;
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
const TMP = '/tmp/igneum-fast-time-dv3';
const BASE = 29790, SUFFIX = 979;
const NEVER = '18446744073709551615';
const args = process.argv.slice(2);
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? Number(args[i + 1]) : dflt; };
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
const BPS = flag('bps', 10);
const PROPAGATION_MS = flag('propagation-ms', 3000);
const DELAY_MS = flag('delay-ms', Math.round(PROPAGATION_MS / 3));
const RULE = sflag('rule') || 'v3';
const EXPECT = sflag('expect') || 'pass';
const SECS = flag('secs', 900);
const MEASURE_FROM = flag('measure-from', 600);
const HANDSHAKE_MS = flag('handshake-ms', 5000);
const GENESIS_BITS = flag('genesis-bits', BPS >= 10 ? 0x1f080000 : 0x1f010000); // 2^13 hashes per block at 10 bps, 2^16 at 1 bps (mantissa x 2^224)
const CASE = sflag('case') || `${BPS}bps-d${PROPAGATION_MS}ms-${RULE}`;
const OUT = sflag('out') || `${ROOT}docs/plans/counter-asic-3-gate/difficulty-v3-${CASE}.json`;
if (!['v2', 'v3'].includes(RULE) || !['pass', 'fail'].includes(EXPECT) || ![1, 10].includes(BPS)) { console.error('usage: --bps 1|10 --delay-ms <ms> --rule v2|v3 --expect pass|fail'); process.exit(2); }
const T_MS = 1000 / BPS;
const started = [];
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
for (const b of [IGNEUMD, CPU_MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
// The override file is merged as TEXT (a never height is beyond a JavaScript number; class-v4.mjs has the history).
const baseText = readFileSync(FILE, 'utf8');
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
export function mergeOverrideText(text, fields) {
let out = text;
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
}
// The block-rate block (the file's is 1 bps). At 10 bps: docs/override-params.md's 10 bps example for k, parents,
// mergeset limit and the sample rates; the depths are the file's x10 (merge 600, finality 7,200, pruning 138,380,
// above the Prunality bound 2F + 4 M k + 2k + 2 = 137,658); the PoW epoch 600 DAA with lead 100 so an epoch stays
// 60 s of wall time and the CPU miners regenerate their program as often as at 1 bps.
export function rerateBlockrate(text, bps) {
if (bps === 1) return text;
const block = `"blockrate": {
"target_time_per_block": 100,
"ghostdag_k": 124,
"past_median_time_sample_rate": 10,
"difficulty_sample_rate": 2,
"max_block_parents": 16,
"mergeset_size_limit": 248,
"merge_depth": 600,
"finality_depth": 7200,
"pruning_depth": 138380,
"coinbase_maturity": 20
}`;
return text.replace(/"blockrate":\s*\{[^}]*\}/, block);
}
const EPOCH = BPS === 1 ? field('pow_epoch_blocks') : 600;
const LEAD = BPS === 1 ? field('pow_epoch_lead') : 100;
const DAY_MS = field('pow_day_ms');
const override = `${TMP}/override.json`;
writeFileSync(override, mergeOverrideText(rerateBlockrate(baseText, BPS), {
genesis_bits: GENESIS_BITS, skip_proof_of_work: false, pow_epoch_blocks: EPOCH, pow_epoch_lead: LEAD,
difficulty_v2_activation_daa: '0', difficulty_v3_activation_daa: RULE === 'v3' ? '0' : NEVER,
program_class_v3_activation_daa: NEVER, program_class_v4_activation_daa: NEVER,
}));
log(`case ${CASE}: ${BPS} bps (T ${T_MS} ms), propagation ${PROPAGATION_MS} ms as a proxy delay of ${DELAY_MS} ms per leg each way (handshake ${HANDSHAKE_MS} ms undelayed), rule ${RULE}, genesis bits 0x${GENESIS_BITS.toString(16)} (${expectedHashes(GENESIS_BITS).toExponential(3)} hashes per block), ${SECS} s measured from ${MEASURE_FROM} s, expect ${EXPECT}`);
// ---- the delay proxy ----
// Every chunk in each direction is written to the other side DELAY_MS after it arrived (timers of one delay fire in
// order, so the byte order holds); the first HANDSHAKE_MS of a connection pass undelayed. Kaspa's flows wait up to
// 120 s (protocol/p2p/src/common.rs DEFAULT_TIMEOUT), so a 30-s delay (60-s round trip) fits inside them.
class DelayProxy {
constructor(listenPort, targetPort) { this.listenPort = listenPort; this.targetPort = targetPort; this.conns = 0; this.bytes = 0; }
start() {
return new Promise((resolve) => {
this.server = createServer((client) => {
this.conns++;
const opened = Date.now();
const upstream = netConnect(this.targetPort, '127.0.0.1');
const pipeDelayed = (from, to) => {
from.on('data', (chunk) => {
this.bytes += chunk.length;
const wait = Date.now() - opened < HANDSHAKE_MS ? 0 : DELAY_MS;
setTimeout(() => { if (!to.destroyed) to.write(chunk); }, wait);
});
from.on('end', () => setTimeout(() => { if (!to.destroyed) to.end(); }, DELAY_MS));
from.on('error', () => { if (!to.destroyed) to.destroy(); });
from.on('close', () => setTimeout(() => { if (!to.destroyed) to.destroy(); }, DELAY_MS + 1000));
};
pipeDelayed(client, upstream);
pipeDelayed(upstream, client);
});
this.server.listen(this.listenPort, '127.0.0.1', () => resolve(this));
});
}
stop() { try { this.server?.close(); } catch { } }
}
class Node {
constructor(i, connect = []) {
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
}
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
async start() {
mkdirSync(this.dir, { recursive: true });
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex',
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`,
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
const out = openSync(this.logFile, 'a');
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] });
started.push(this.proc);
await sleep(1500);
if (this.proc.exitCode != null) throw new Error(`n${this.i} exited ${this.proc.exitCode}: ${this.grepLog(/ERROR|Error|error|refused|invalid/).slice(-3).join(' | ')}`);
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}${this.connect.length ? ` via proxy ${this.connect.join(',')}` : ''}`);
return this;
}
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
}
function miner(bin, argv, name, env = {}) {
const out = openSync(`${TMP}/${name}.log`, 'a');
const p = spawn(bin, argv, { stdio: ['ignore', out, out], env: { ...process.env, ...env } });
started.push(p);
return p;
}
const proxies = [];
async function stopAll() {
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
await sleep(1500);
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
for (const x of proxies) x.stop();
}
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
// bits -> expected hashes per block: target = mantissa x 2^(8 (exponent - 3)); hashes = 2^256 / (target + 1)
export function expectedHashes(bits) {
const exp = BigInt(bits >>> 24), mant = BigInt(bits & 0xffffff);
const target = exp <= 3n ? mant >> (8n * (3n - exp)) : mant << (8n * (exp - 3n));
const ratio = (1n << 256n) / (target + 1n);
return Number(ratio);
}
const minerHash = (i) => { // the newest `hash=<x> MH/s` status line of miner i, in hashes per second
let v = null;
try { for (const l of readFileSync(`${TMP}/cpu${i}.log`, 'utf8').split('\n')) { const m = /hash=([\d.]+) MH\/s/.exec(l); if (m) v = +m[1] * 1e6; } } catch { }
return v;
};
const n0 = await new Node(0).start();
const p1 = await new DelayProxy(BASE + 101, n0.p2pPort).start(); proxies.push(p1);
const p2 = await new DelayProxy(BASE + 102, n0.p2pPort).start(); proxies.push(p2);
const n1 = await new Node(1, [`127.0.0.1:${p1.listenPort}`]).start();
const n2 = await new Node(2, [`127.0.0.1:${p2.listenPort}`]).start();
const nodes = [n0, n1, n2];
await sleep(3000);
for (const n of nodes) { const d = await n.rpc.call('getBlockDagInfo'); log(`n${n.i} network ${d.network ?? d.networkName} blocks ${d.blockCount} difficulty ${d.difficulty} (bits ${d.difficulty * 2} hashes per block)`); }
const peers = await Promise.all(nodes.map(async n => { try { const r = await n.rpc.call('getConnectedPeerInfo'); const arr = Object.values(r).find(Array.isArray); return arr ? arr.length : '?'; } catch { return '?'; } }));
log(`peers per node ${peers.join(' ')} (hub 2, spokes 1)`);
nodes.forEach((n, i) => miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS + 30), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) }));
void devAddress('fast-time-dv3');
const t0 = Date.now();
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
const samples = [];
let lastReport = 0, measureStart = null;
while (Date.now() - t0 < SECS * 1000) {
await sleep(5000);
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return null; } }));
let bits = null;
try { const b = await n0.rpc.call('getBlock', { hash: dag[0].sink, includeTransactions: false }); bits = +b.block.header.bits; } catch { }
const hashes = [0, 1, 2].map(minerHash);
const hashSum = hashes.every(h => h != null) ? hashes.reduce((a, b) => a + b, 0) : null;
const expected = bits == null ? null : expectedHashes(bits);
const implied = hashSum == null ? null : hashSum * T_MS / 1000;
const s = { t: +since(), blocks: dag.map(d => d?.blockCount ?? null), daa: dag[0]?.virtualDaaScore ?? null, difficulty: dag[0]?.difficulty ?? null, bits, expected_hashes: expected, miner_hash: hashSum, hash_implied: implied, ratio: expected != null && implied ? expected / implied : null };
samples.push(s);
if (measureStart == null && s.t >= MEASURE_FROM && s.blocks[0] != null) measureStart = s;
if (Date.now() - lastReport > 30000) {
lastReport = Date.now();
const rate = samples.length > 6 ? ((s.blocks[0] - samples.at(-7).blocks[0]) / (s.t - samples.at(-7).t)).toFixed(2) : '?';
log(`t=${since()} s blocks ${s.blocks.join('/')} daa ${s.daa} rate(30s) ${rate}/s bits 0x${(bits ?? 0).toString(16)} expected ${expected?.toExponential(3)} hashes/block, miners ${hashSum == null ? '?' : (hashSum / 1e3).toFixed(1) + ' kH/s'} implied ${implied?.toExponential(3)} ratio ${s.ratio?.toFixed(3)}`);
}
}
await sleep(2000);
// ---- the reading ----
const end = samples.at(-1);
const measured = samples.filter(s => s.t >= MEASURE_FROM && s.ratio != null);
const dagRate = measureStart && end.blocks[0] != null ? (end.blocks[0] - measureStart.blocks[0]) / (end.t - measureStart.t) : null;
const rateOff = dagRate == null ? null : dagRate / BPS - 1;
const ratioMean = measured.length ? measured.reduce((a, s) => a + s.ratio, 0) / measured.length : null;
const diffOff = ratioMean == null ? null : ratioMean - 1;
// the red share and the chain step spacing from the hub's blocks since the measured window began
let reds = 0, chainBlocks = 0, merged = 0, spacings = [];
try {
const info = await n0.rpc.call('getBlockDagInfo');
let low = info.pruningPointHash; const seen = new Set(); let prevChainClock = null;
for (let round = 0; round < 400; round++) {
const r = await n0.rpc.call('getBlocks', { lowHash: low, includeBlocks: true, includeTransactions: false });
const blocks = r.blocks || []; let added = 0;
for (const b of blocks) {
const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h); added++;
if (!b.verboseData?.isChainBlock) continue;
const tMs = +b.header.timestamp;
if (tMs < t0 + MEASURE_FROM * 1000) continue;
chainBlocks++; reds += (b.verboseData.mergeSetRedsHashes || []).length; merged += (b.verboseData.mergeSetBluesHashes || []).length + (b.verboseData.mergeSetRedsHashes || []).length;
if (prevChainClock != null) spacings.push(tMs - prevChainClock); prevChainClock = tMs;
}
if (!blocks.length || added === 0) break;
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
}
} catch (e) { log(`getBlocks: ${e.message}`); }
spacings.sort((a, b) => a - b);
const median = spacings.length ? spacings[Math.floor(spacings.length / 2)] : null;
const checks = {
measured_window_present: measured.length >= 10,
dag_rate_within_10pct: rateOff != null && Math.abs(rateOff) <= 0.10,
difficulty_within_10pct_of_hash: diffOff != null && Math.abs(diffOff) <= 0.10,
three_nodes_within_5pct_blocks: end.blocks.every(b => b != null) && Math.max(...end.blocks) - Math.min(...end.blocks) <= 0.05 * Math.max(...end.blocks),
};
const good = checks.measured_window_present && checks.dag_rate_within_10pct && checks.difficulty_within_10pct_of_hash;
const pass = EXPECT === 'pass' ? good : !good;
const summary = {
pass, expect: EXPECT, case: CASE, bps: BPS, target_time_ms: T_MS, propagation_ms: PROPAGATION_MS, delay_ms_per_leg: DELAY_MS, handshake_ms: HANDSHAKE_MS, rule: RULE, genesis_bits: GENESIS_BITS, secs: SECS, measure_from: MEASURE_FROM,
checks, dag_rate_measured: dagRate, dag_rate_off: rateOff, difficulty_over_hash_implied_mean: ratioMean, difficulty_off: diffOff,
measured_samples: measured.length, end_blocks: end.blocks, end_daa: end.daa, end_bits: end.bits, end_expected_hashes: end.expected_hashes, end_miner_hash: end.miner_hash,
chain_blocks_measured: chainBlocks, merged_blocks_measured: merged, red_blocks_measured: reds, red_share: merged ? reds / merged : null,
chain_step_spacing_ms: { median, p90: spacings.length ? spacings[Math.floor(spacings.length * 0.9)] : null, max: spacings.at(-1) ?? null },
proxy: { connections: p1.conns + p2.conns, bytes: p1.bytes + p2.bytes },
node: IGNEUMD, miner: CPU_MINER, samples,
};
mkdirSync(OUT.replace(/\/[^/]+$/, ''), { recursive: true });
writeFileSync(OUT, JSON.stringify(summary, null, 2));
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
const fails = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'} (expect ${EXPECT}, ${CASE}): DAG rate ${dagRate?.toFixed(3)}/s against ${BPS} (${rateOff == null ? '?' : (100 * rateOff).toFixed(1) + '%'}), difficulty / hash-implied ${ratioMean?.toFixed(3)} (${diffOff == null ? '?' : (100 * diffOff).toFixed(1) + '%'}) over ${measured.length} samples; red share ${summary.red_share == null ? '?' : (100 * summary.red_share).toFixed(1) + '%'} of ${merged} merged over ${chainBlocks} chain blocks, chain step median ${median} ms p90 ${summary.chain_step_spacing_ms.p90} ms; blocks ${end.blocks.join('/')}; proxy ${summary.proxy.connections} conns ${summary.proxy.bytes} bytes${fails.length ? `; FAILED CHECK ${fails.join(', ')}` : ''}`);
log(`summary: ${OUT}`);
await stopAll();
process.exit(pass ? 0 : 1);