Merge commit '49884b9' into ca3-v4-hash

This commit is contained in:
igneum-labs 2026-10-06 16:24:47 +00:00
commit 71dc050822
56 changed files with 3424 additions and 145 deletions

View file

@ -69,6 +69,8 @@ jobs:
run: bash tools/ci/no-conflict-markers.sh
- name: copied sources are re-stamped before a build
run: bash tools/ci/copied-sources-check.sh
- name: override params files parse with no duplicate key (the duplicate-field class, 6 October 2026)
run: bash tools/ci/override-json-check.sh
- name: the signer is never piped into head
run: bash tools/ci/signer-pipe-check.sh
- name: bash bodies in PowerShell job scripts pass bash -n, the lost-quote class (self-test first, then the tree)

View file

@ -0,0 +1,384 @@
{
"pass": true,
"checks": {
"v4_switch_line_on_every_node": true,
"v4_switch_line_names_the_rounded_epoch": true,
"v3_switch_line_on_every_node": true,
"template_switched_at_the_first_v4_epoch": true,
"template_switched_at_the_first_v3_epoch": true,
"blocks_before_the_boundary": true,
"blocks_after_the_boundary": true,
"v3_and_v4_programs_seen": true,
"v2_programs_seen_when_v3_starts_later": true,
"program_ids_differ_across_the_switch": true,
"miners_agree_on_every_program": true,
"zero_rejected_by_miners": true,
"zero_rejected_by_nodes": true,
"sinks_agree": true,
"block_counts_agree": true
},
"activation": 150,
"v3_activation": 60,
"epoch_blocks": 60,
"first_v3_epoch": 1,
"first_v4_epoch": 3,
"boundary_daa": 180,
"secs": 420,
"threads": 1,
"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",
"metal_worker": null,
"genesis_bits": "0x1f010000",
"template_switch_v3": {
"epoch": 1,
"daa": 60,
"at": 25.7
},
"template_switch": {
"epoch": 3,
"daa": 180,
"at": 163.9
},
"run_ended_at_s": 285,
"final_daa": 304,
"blocks": {
"total": 309,
"before_boundary": 181,
"after_boundary": 128,
"chain_before": 177,
"chain_after": 127
},
"programs": [
{
"epoch": 0,
"class": "v2",
"program_id": "8f8806638d59850f",
"seed": "234e082d653dc69d",
"miners": 3,
"disagree": false,
"ready_ms": [
198,
199,
199
]
},
{
"epoch": 1,
"class": "v3",
"program_id": "29ced0d031cc121b",
"seed": "bb01d0cccbc2ea2e",
"miners": 3,
"disagree": false,
"ready_ms": [
187,
186,
187
]
},
{
"epoch": 2,
"class": "v3",
"program_id": "341fd4076256cd7b",
"seed": "060caf3a3e006592",
"miners": 3,
"disagree": false,
"ready_ms": [
2,
2,
2
]
},
{
"epoch": 3,
"class": "v4",
"program_id": "070ca0f94e7f8f88",
"seed": "cc9d1bb4443c3028",
"miners": 3,
"disagree": false,
"ready_ms": [
2,
2,
2
]
},
{
"epoch": 4,
"class": "v4",
"program_id": "b8cbe1ef7ab1d748",
"seed": "54e2edcfe251360c",
"miners": 3,
"disagree": false,
"ready_ms": [
2,
2,
2
]
},
{
"epoch": 5,
"class": "v4",
"program_id": "9321cbd9c54c0b83",
"seed": "53a2fe6ce1308fb3",
"miners": 3,
"disagree": false,
"ready_ms": [
2,
2,
2
]
}
],
"accepted_per_miner": [
103,
103,
102
],
"rejected_by_miners": [
0,
0,
0
],
"rejected_by_nodes": [
0,
0,
0
],
"rejected_lines": [],
"sinks": [
"4da9ac669effdfab",
"4da9ac669effdfab",
"4da9ac669effdfab"
],
"block_counts": [
308,
308,
308
],
"tips_per_node": [
1,
1,
1
],
"switch_lines": [
"Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)"
],
"v3_switch_lines": [
"Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)",
"Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)",
"Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)"
],
"samples": [
{
"t": 4.6,
"daa": 0,
"epoch": 0,
"class": 2,
"nodes": [
"0/234e082d",
"0/234e082d",
"0/234e082d"
]
},
{
"t": 19.7,
"daa": 51,
"epoch": 0,
"class": 2,
"nodes": [
"51/b6699ac7",
"51/b6699ac7",
"51/b6699ac7"
]
},
{
"t": 34.7,
"daa": 62,
"epoch": 1,
"class": 3,
"nodes": [
"62/50a9cf7a",
"62/50a9cf7a",
"62/50a9cf7a"
]
},
{
"t": 49.7,
"daa": 68,
"epoch": 1,
"class": 3,
"nodes": [
"68/e0145a0c",
"68/e0145a0c",
"68/e0145a0c"
]
},
{
"t": 64.7,
"daa": 76,
"epoch": 1,
"class": 3,
"nodes": [
"76/c23ad948",
"76/c23ad948",
"76/c23ad948"
]
},
{
"t": 79.8,
"daa": 97,
"epoch": 1,
"class": 3,
"nodes": [
"97/b039ce4c",
"97/b039ce4c",
"97/b039ce4c"
]
},
{
"t": 94.8,
"daa": 113,
"epoch": 1,
"class": 3,
"nodes": [
"113/ae890f91",
"113/ae890f91",
"113/ae890f91"
]
},
{
"t": 109.8,
"daa": 128,
"epoch": 2,
"class": 3,
"nodes": [
"128/3545bd70",
"128/3545bd70",
"128/3545bd70"
]
},
{
"t": 124.8,
"daa": 138,
"epoch": 2,
"class": 3,
"nodes": [
"138/5e342f39",
"138/5e342f39",
"138/5e342f39"
]
},
{
"t": 139.8,
"daa": 152,
"epoch": 2,
"class": 3,
"nodes": [
"152/f376cfe3",
"152/f376cfe3",
"152/f376cfe3"
]
},
{
"t": 154.9,
"daa": 168,
"epoch": 2,
"class": 3,
"nodes": [
"168/18974e58",
"168/18974e58",
"168/18974e58"
]
},
{
"t": 169.9,
"daa": 189,
"epoch": 3,
"class": 4,
"nodes": [
"189/2efab18a",
"189/2efab18a",
"189/2efab18a"
]
},
{
"t": 184.9,
"daa": 203,
"epoch": 3,
"class": 4,
"nodes": [
"203/024556df",
"203/024556df",
"203/024556df"
]
},
{
"t": 199.9,
"daa": 215,
"epoch": 3,
"class": 4,
"nodes": [
"215/6aa4e4c9",
"215/6aa4e4c9",
"215/6aa4e4c9"
]
},
{
"t": 214.9,
"daa": 230,
"epoch": 3,
"class": 4,
"nodes": [
"230/54e2edcf",
"230/54e2edcf",
"230/54e2edcf"
]
},
{
"t": 230,
"daa": 245,
"epoch": 4,
"class": 4,
"nodes": [
"245/f5fc54f8",
"245/f5fc54f8",
"245/f5fc54f8"
]
},
{
"t": 245,
"daa": 265,
"epoch": 4,
"class": 4,
"nodes": [
"265/7fe10368",
"265/7fe10368",
"265/7fe10368"
]
},
{
"t": 260,
"daa": 278,
"epoch": 4,
"class": 4,
"nodes": [
"278/d50d4e8e",
"278/d50d4e8e",
"278/d50d4e8e"
]
},
{
"t": 275,
"daa": 287,
"epoch": 4,
"class": 4,
"nodes": [
"287/83dc90ba",
"287/83dc90ba",
"287/83dc90ba"
]
}
],
"metal": null
}

View file

@ -0,0 +1,447 @@
{
"pass": true,
"checks": {
"v4_switch_line_on_every_node": true,
"v4_switch_line_names_the_rounded_epoch": true,
"v3_switch_line_on_every_node": true,
"template_switched_at_the_first_v4_epoch": true,
"template_switched_at_the_first_v3_epoch": true,
"blocks_before_the_boundary": true,
"blocks_after_the_boundary": true,
"v3_and_v4_programs_seen": true,
"v2_programs_seen_when_v3_starts_later": true,
"program_ids_differ_across_the_switch": true,
"miners_agree_on_every_program": true,
"zero_rejected_by_miners": true,
"zero_rejected_by_nodes": true,
"sinks_agree": true,
"block_counts_agree": true,
"metal_prepare_sent_for_v4": true,
"metal_worker_prepared_v4_pack": true,
"metal_no_need_or_mismatch": true,
"metal_accepted_blocks_after_switch": true,
"metal_cpu_recheck_clean": true,
"metal_swapped_without_pause": true
},
"activation": 150,
"v3_activation": 60,
"epoch_blocks": 60,
"first_v3_epoch": 1,
"first_v4_epoch": 3,
"boundary_daa": 180,
"secs": 480,
"threads": 1,
"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",
"metal_worker": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/proto-metal/igneum-bench",
"genesis_bits": "0x1e010000",
"template_switch_v3": {
"epoch": 1,
"daa": 60,
"at": 83.1
},
"template_switch": {
"epoch": 3,
"daa": 180,
"at": 191.7
},
"run_ended_at_s": 317.5,
"final_daa": 300,
"blocks": {
"total": 304,
"before_boundary": 182,
"after_boundary": 122,
"chain_before": 93,
"chain_after": 63
},
"programs": [
{
"epoch": 0,
"class": "v2",
"program_id": "3e974094b5ce543f",
"seed": "130e3b7d568db07a",
"miners": 2,
"disagree": false,
"ready_ms": [
179,
180
]
},
{
"epoch": 1,
"class": "v3",
"program_id": "8ef0132ff054616a",
"seed": "b515430faf46fda8",
"miners": 2,
"disagree": false,
"ready_ms": [
291,
290
]
},
{
"epoch": 2,
"class": "v3",
"program_id": "d3da91ec49682ce1",
"seed": "9cbe1982e1eaf9df",
"miners": 2,
"disagree": false,
"ready_ms": [
7,
7
]
},
{
"epoch": 3,
"class": "v4",
"program_id": "2bcecca6f867c3b5",
"seed": "ca4245b598fcbeb4",
"miners": 2,
"disagree": false,
"ready_ms": [
7,
7
]
},
{
"epoch": 4,
"class": "v4",
"program_id": "21b78e63fc0e9251",
"seed": "3b6cfced206edf85",
"miners": 2,
"disagree": false,
"ready_ms": [
8,
7
]
},
{
"epoch": 5,
"class": "v4",
"program_id": "3855570a5ba12fc9",
"seed": "343510d7149dcf50",
"miners": 2,
"disagree": false,
"ready_ms": [
7,
7
]
}
],
"accepted_per_miner": [
301,
1,
1
],
"rejected_by_miners": [
0,
0,
0
],
"rejected_by_nodes": [
0,
0,
0
],
"rejected_lines": [],
"sinks": [
"4e1aa3fbf1f4a54b",
"4e1aa3fbf1f4a54b",
"4e1aa3fbf1f4a54b"
],
"block_counts": [
303,
303,
303
],
"tips_per_node": [
2,
2,
2
],
"switch_lines": [
"Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)",
"Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)"
],
"v3_switch_lines": [
"Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)",
"Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)",
"Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)"
],
"samples": [
{
"t": 4.6,
"daa": 0,
"epoch": 0,
"class": 2,
"nodes": [
"0/130e3b7d",
"0/130e3b7d",
"0/130e3b7d"
]
},
{
"t": 19.7,
"daa": 2,
"epoch": 0,
"class": 2,
"nodes": [
"2/a26633e3",
"2/a26633e3",
"2/a26633e3"
]
},
{
"t": 34.7,
"daa": 4,
"epoch": 0,
"class": 2,
"nodes": [
"4/2e80b7f2",
"4/2e80b7f2",
"4/2e80b7f2"
]
},
{
"t": 49.7,
"daa": 14,
"epoch": 0,
"class": 2,
"nodes": [
"14/b6297491",
"14/b6297491",
"14/b6297491"
]
},
{
"t": 64.9,
"daa": 33,
"epoch": 0,
"class": 2,
"nodes": [
"33/41f3c99e",
"33/41f3c99e",
"33/41f3c99e"
]
},
{
"t": 80.1,
"daa": 54,
"epoch": 0,
"class": 2,
"nodes": [
"54/e14baafe",
"54/e14baafe",
"54/e14baafe"
]
},
{
"t": 95.1,
"daa": 76,
"epoch": 1,
"class": 3,
"nodes": [
"76/31410d82",
"76/31410d82",
"76/31410d82"
]
},
{
"t": 110.2,
"daa": 94,
"epoch": 1,
"class": 3,
"nodes": [
"94/f17df5db",
"94/f17df5db",
"94/f17df5db"
]
},
{
"t": 125.3,
"daa": 111,
"epoch": 1,
"class": 3,
"nodes": [
"111/9cbe1982",
"111/9cbe1982",
"111/9cbe1982"
]
},
{
"t": 140.5,
"daa": 125,
"epoch": 2,
"class": 3,
"nodes": [
"125/185ebceb",
"125/185ebceb",
"125/185ebceb"
]
},
{
"t": 155.8,
"daa": 144,
"epoch": 2,
"class": 3,
"nodes": [
"144/904395a2",
"144/904395a2",
"144/904395a2"
]
},
{
"t": 171.2,
"daa": 158,
"epoch": 2,
"class": 3,
"nodes": [
"158/e5faf4c5",
"158/e5faf4c5",
"158/e5faf4c5"
]
},
{
"t": 186.6,
"daa": 174,
"epoch": 2,
"class": 3,
"nodes": [
"174/1a963704",
"174/1a963704",
"174/1a963704"
]
},
{
"t": 201.8,
"daa": 190,
"epoch": 3,
"class": 4,
"nodes": [
"190/51e300b8",
"190/51e300b8",
"190/51e300b8"
]
},
{
"t": 217,
"daa": 205,
"epoch": 3,
"class": 4,
"nodes": [
"205/251d965c",
"205/251d965c",
"205/251d965c"
]
},
{
"t": 232.1,
"daa": 219,
"epoch": 3,
"class": 4,
"nodes": [
"219/0ecdb99f",
"219/0ecdb99f",
"219/0ecdb99f"
]
},
{
"t": 247.3,
"daa": 234,
"epoch": 3,
"class": 4,
"nodes": [
"234/9f6a3235",
"233/686123b5",
"233/686123b5"
]
},
{
"t": 262.4,
"daa": 247,
"epoch": 4,
"class": 4,
"nodes": [
"247/da402167",
"247/da402167",
"247/da402167"
]
},
{
"t": 277.6,
"daa": 263,
"epoch": 4,
"class": 4,
"nodes": [
"263/86fe7193",
"263/86fe7193",
"263/86fe7193"
]
},
{
"t": 292.8,
"daa": 278,
"epoch": 4,
"class": 4,
"nodes": [
"278/6f950a2e",
"278/6f950a2e",
"278/6f950a2e"
]
},
{
"t": 308.3,
"daa": 293,
"epoch": 4,
"class": 4,
"nodes": [
"293/c29bced3",
"293/c29bced3",
"293/c29bced3"
]
}
],
"metal": {
"worker": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/proto-metal/igneum-bench",
"prepares": [
"PREPARE sent for epoch seed b515430faf46fda81a323a803d58c5f3ef6a6777d1ca2b83cbd41ca0e45fe51d day 1243960 class v3 (6 DAA blocks before the boundary at 60; CPU side ready in 2280 ms)",
"PREPARE sent for epoch seed 9cbe1982e1eaf9df5c1a9dbbf870ca7016987c4594cd3cd47505f9827d1bed70 day 1243960 class v3 (6 DAA blocks before the boundary at 120; CPU side ready in 2071 ms)",
"PREPARE sent for epoch seed ca4245b598fcbeb49449c5072ae0cc6d3803eeb21f75da1eed5a56e8439bdb91 day 1243960 class v4 (6 DAA blocks before the boundary at 180; CPU side ready in 2599 ms)",
"PREPARE sent for epoch seed 3b6cfced206edf85aa0e643ee81c4c01c9a21a3de16e0c2d7347da27540867fa day 1243960 class v4 (6 DAA blocks before the boundary at 240; CPU side ready in 2325 ms)",
"PREPARE sent for epoch seed 343510d7149dcf50f3d302d9e7ed8cd0526dcadf4904ceafa74506e438c49031 day 1243960 class v4 (5 DAA blocks before the boundary at 300; CPU side ready in 2168 ms)"
],
"prepared": [
"worker: prepared b515430faf46fda81a323a803d58c5f3ef6a6777d1ca2b83cbd41ca0e45fe51d 69676e65756d2d6461792f38fb120000000000 396.1 program 62.9 dataset 333.1 race 0.0 variant base class v3 loads/hash 128 cache-fill 0.8 build 174.9 resident 2 programs 2 datasets (prepare answered 0.4 s after it was sent)",
"worker: prepared 9cbe1982e1eaf9df5c1a9dbbf870ca7016987c4594cd3cd47505f9827d1bed70 69676e65756d2d6461792f38fb120000000000 61.7 program 61.7 dataset 0.0 race 0.0 variant base class v3 loads/hash 128 cache-fill 0.8 build 174.9 resident 2 programs 2 datasets (prepare answered 0.1 s after it was sent)",
"worker: prepared ca4245b598fcbeb49449c5072ae0cc6d3803eeb21f75da1eed5a56e8439bdb91 69676e65756d2d6461792f38fb120000000000 430.2 program 193.9 dataset 236.2 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.4 s after it was sent)",
"worker: prepared 3b6cfced206edf85aa0e643ee81c4c01c9a21a3de16e0c2d7347da27540867fa 69676e65756d2d6461792f38fb120000000000 138.6 program 138.5 dataset 0.0 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.1 s after it was sent)",
"worker: prepared 343510d7149dcf50f3d302d9e7ed8cd0526dcadf4904ceafa74506e438c49031 69676e65756d2d6461792f38fb120000000000 175.3 program 175.3 dataset 0.0 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.2 s after it was sent)"
],
"prepared_v4": [
"worker: prepared ca4245b598fcbeb49449c5072ae0cc6d3803eeb21f75da1eed5a56e8439bdb91 69676e65756d2d6461792f38fb120000000000 430.2 program 193.9 dataset 236.2 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.4 s after it was sent)",
"worker: prepared 3b6cfced206edf85aa0e643ee81c4c01c9a21a3de16e0c2d7347da27540867fa 69676e65756d2d6461792f38fb120000000000 138.6 program 138.5 dataset 0.0 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.1 s after it was sent)",
"worker: prepared 343510d7149dcf50f3d302d9e7ed8cd0526dcadf4904ceafa74506e438c49031 69676e65756d2d6461792f38fb120000000000 175.3 program 175.3 dataset 0.0 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.2 s after it was sent)"
],
"prepared_v3": [
"worker: prepared b515430faf46fda81a323a803d58c5f3ef6a6777d1ca2b83cbd41ca0e45fe51d 69676e65756d2d6461792f38fb120000000000 396.1 program 62.9 dataset 333.1 race 0.0 variant base class v3 loads/hash 128 cache-fill 0.8 build 174.9 resident 2 programs 2 datasets (prepare answered 0.4 s after it was sent)",
"worker: prepared 9cbe1982e1eaf9df5c1a9dbbf870ca7016987c4594cd3cd47505f9827d1bed70 69676e65756d2d6461792f38fb120000000000 61.7 program 61.7 dataset 0.0 race 0.0 variant base class v3 loads/hash 128 cache-fill 0.8 build 174.9 resident 2 programs 2 datasets (prepare answered 0.1 s after it was sent)"
],
"need": 0,
"mismatch_lines": [],
"rebuild_lines_before_v4": [],
"refused": [],
"swaps": [
"SEED CHANGE at daa 60: epoch seed 130e3b7d568db07aad638f67b2ac26e7cf93c51e4f564d45c6724fc4b2d3da65 -> b515430faf46fda81a323a803d58c5f3ef6a6777d1ca2b83cbd41ca0e45fe51d, day 1243960 -> 1243960 (epoch 1): swapped with no pause (prepared 2 s ago, prepare took 396 ms)",
"SEED CHANGE at daa 120: epoch seed b515430faf46fda81a323a803d58c5f3ef6a6777d1ca2b83cbd41ca0e45fe51d -> 9cbe1982e1eaf9df5c1a9dbbf870ca7016987c4594cd3cd47505f9827d1bed70, day 1243960 -> 1243960 (epoch 2): swapped with no pause (prepared 4 s ago, prepare took 62 ms)",
"SEED CHANGE at daa 180: epoch seed 9cbe1982e1eaf9df5c1a9dbbf870ca7016987c4594cd3cd47505f9827d1bed70 -> ca4245b598fcbeb49449c5072ae0cc6d3803eeb21f75da1eed5a56e8439bdb91, day 1243960 -> 1243960 (epoch 3): swapped with no pause (prepared 3 s ago, prepare took 430 ms)",
"SEED CHANGE at daa 240: epoch seed ca4245b598fcbeb49449c5072ae0cc6d3803eeb21f75da1eed5a56e8439bdb91 -> 3b6cfced206edf85aa0e643ee81c4c01c9a21a3de16e0c2d7347da27540867fa, day 1243960 -> 1243960 (epoch 4): swapped with no pause (prepared 4 s ago, prepare took 139 ms)",
"SEED CHANGE at daa 300: epoch seed 3b6cfced206edf85aa0e643ee81c4c01c9a21a3de16e0c2d7347da27540867fa -> 343510d7149dcf50f3d302d9e7ed8cd0526dcadf4904ceafa74506e438c49031, day 1243960 -> 1243960 (epoch 5): swapped with no pause (prepared 5 s ago, prepare took 175 ms)"
],
"accepted_total": 301,
"accepted_after_switch": 123,
"found_lines": 0,
"cpu_recheck_mismatched": 0,
"last_status": ""
}
}

View file

@ -0,0 +1,245 @@
{
"pass": false,
"checks": {
"v4_switch_line_on_every_node": true,
"v4_switch_line_names_the_rounded_epoch": false,
"v3_switch_line_on_every_node": true,
"template_switched_at_the_first_v4_epoch": false,
"template_switched_at_the_first_v3_epoch": true,
"blocks_before_the_boundary": true,
"blocks_after_the_boundary": false,
"v3_and_v4_programs_seen": false,
"v2_programs_seen_when_v3_starts_later": true,
"program_ids_differ_across_the_switch": false,
"miners_agree_on_every_program": true,
"zero_rejected_by_miners": true,
"zero_rejected_by_nodes": true,
"sinks_agree": true,
"block_counts_agree": true
},
"activation": "never",
"v3_activation": 60,
"epoch_blocks": 60,
"first_v3_epoch": 1,
"first_v4_epoch": null,
"boundary_daa": "never",
"secs": 150,
"threads": 1,
"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",
"metal_worker": null,
"genesis_bits": "0x1f010000",
"template_switch_v3": {
"epoch": 1,
"daa": 60,
"at": 31.7
},
"template_switch": null,
"run_ended_at_s": null,
"final_daa": 164,
"blocks": {
"total": 167,
"before_boundary": 167,
"after_boundary": 0,
"chain_before": 165,
"chain_after": 0
},
"programs": [
{
"epoch": 0,
"class": "v2",
"program_id": "8f8806638d59850f",
"seed": "234e082d653dc69d",
"miners": 3,
"disagree": false,
"ready_ms": [
209,
203,
208,
206,
209,
203
]
},
{
"epoch": 1,
"class": "v3",
"program_id": "89059149b7328e64",
"seed": "80127533e53a2df7",
"miners": 3,
"disagree": false,
"ready_ms": [
193,
192,
192
]
},
{
"epoch": 2,
"class": "v3",
"program_id": "6da8a5eb4dff139a",
"seed": "0e93562ee5369f59",
"miners": 3,
"disagree": false,
"ready_ms": [
3,
4,
4
]
}
],
"accepted_per_miner": [
46,
59,
61
],
"rejected_by_miners": [
0,
0,
0
],
"rejected_by_nodes": [
0,
0,
0
],
"rejected_lines": [],
"sinks": [
"20e729b7e4ef8637",
"20e729b7e4ef8637",
"20e729b7e4ef8637"
],
"block_counts": [
166,
166,
166
],
"tips_per_node": [
1,
1,
1
],
"switch_lines": [
"Program class v4 from the override file: never",
"Program class v4 from the override file: never",
"Program class v4 from the override file: never"
],
"v3_switch_lines": [
"Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)",
"Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)",
"Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)"
],
"samples": [
{
"t": 4.6,
"daa": 0,
"epoch": 0,
"class": 2,
"nodes": [
"0/234e082d",
"0/234e082d",
"0/234e082d"
]
},
{
"t": 19.7,
"daa": 39,
"epoch": 0,
"class": 2,
"nodes": [
"39/14b03ba6",
"39/14b03ba6",
"39/14b03ba6"
]
},
{
"t": 34.7,
"daa": 62,
"epoch": 1,
"class": 3,
"nodes": [
"62/b22630fc",
"62/b22630fc",
"62/b22630fc"
]
},
{
"t": 49.7,
"daa": 68,
"epoch": 1,
"class": 3,
"nodes": [
"68/c8bb2f04",
"68/c8bb2f04",
"68/c8bb2f04"
]
},
{
"t": 64.7,
"daa": 83,
"epoch": 1,
"class": 3,
"nodes": [
"83/d2610654",
"83/d2610654",
"83/d2610654"
]
},
{
"t": 79.8,
"daa": 101,
"epoch": 1,
"class": 3,
"nodes": [
"101/72beef65",
"101/72beef65",
"101/72beef65"
]
},
{
"t": 94.8,
"daa": 111,
"epoch": 1,
"class": 3,
"nodes": [
"111/846008b8",
"111/846008b8",
"111/846008b8"
]
},
{
"t": 109.8,
"daa": 121,
"epoch": 2,
"class": 3,
"nodes": [
"121/d4da8120",
"121/d4da8120",
"121/d4da8120"
]
},
{
"t": 124.8,
"daa": 134,
"epoch": 2,
"class": 3,
"nodes": [
"134/c7e80324",
"134/c7e80324",
"134/c7e80324"
]
},
{
"t": 139.8,
"daa": 152,
"epoch": 2,
"class": 3,
"nodes": [
"152/2a916f0d",
"152/2a916f0d",
"152/2a916f0d"
]
}
],
"metal": null
}

View file

@ -0,0 +1,12 @@
# Counter ASIC 3.0: the node-side gates for class v4 (`mx8+sh256x27`)
6 October 2026, worker "ca3-v4-node". The rows in the shape of `docs/plans/counter-asic-2-rollout.md` section 7. Trees: main `ca3-v4-node` a522d04 (igneum-pow, the workers, the harness, on ca3-coord 3213ee9), fork `ca3-v4-node` 5f7e0543 (on release-0.3.13-node bb43e9a8, the current release tip, which contains release-0.3.12-node). Builds: the Mac built igneumd and igneum-miner (`target-ca3v4/release`, `--features kaspad/igneum-pow`, warm from an APFS clone of the 0.3.13 target dir, under `with-lock.sh build`, nice 19, 4 jobs) and `igneum-bench` (the Metal worker, `swiftc -O`); the suites went to PC 2. The Mac's installed app was not touched (its miner stays paused); nothing was published; the live devnet was not touched. The Mac's load average during the runs: 5 to 12 (other agents' builds); the gates here count blocks, lines and ids, not rates, so the load does not move them.
| # | Gate | Evidence required | State |
|---|---|---|---|
| G4 | the fast-time 3-node network mining across a v4 activation | 0 rejected blocks, 0 forks, every node's first lines show the switch, blocks on both sides of the boundary; the gate fires on one finished and one failed case | GREEN. Run 1 (15:54:32 to 15:59:21Z, `node infra/fast-time/class-v4.mjs --secs 420 --activation 150 --v3-activation 60 --epochs-after 2` under `with-lock.sh run`; three nodes, one real CPU miner each with `--engine igneum-pow`, every node verifying the other two): 3 of 3 nodes print both switch lines (`Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)` and the v3 line for epoch 1 at DAA 60); templates class 2 for epoch 0, class 3 for epochs 1 and 2, class 4 for epochs 3 to 5; the switch seen at DAA 180, 163.9 s wall; 181 blocks before and 128 after DAA 180 (309 in all, selected chain 177 / 127); program ids agree on all three miners (e0 v2 `8f8806638d59850f`, the same v2 id as 2.0's three runs; e1 v3 `29ced0d031cc121b`, e2 v3 `341fd4076256cd7b`, e3 v4 `070ca0f94e7f8f88`, e4 v4 `b8cbe1ef7ab1d748`, e5 v4 `9321cbd9c54c0b83`; no v2 or v3 id reappears under v4); rejected 0/0/0 on the miners and 0/0/0 on the nodes (103, 103, 102 accepted); one sink `4da9ac669effdfab` on all three at 308/308/308 blocks, one tip each; digest `59a3eb5383bfbf23...` on all three; program and cache ready on one core: v2 epoch 0 199 ms, the first v3 epoch 187 ms (its own day cache), the first v4 epoch 2 ms (the v3 day cache reused across the N5 boundary, no rebuild). Summary `class-v4-20261006-1553Z-cpu.json`. Run 2, the known-failed case (15:59:46 to 16:02:17Z, `--activation never --v3-activation 60 --secs 150`): every node prints `Program class v4 from the override file: never`; the harness reports `SUMMARY FAIL: NO v4 epoch seen` with five checks down (`v4_switch_line_names_the_rounded_epoch`, `template_switched_at_the_first_v4_epoch`, `blocks_after_the_boundary`, `v3_and_v4_programs_seen`, `program_ids_differ_across_the_switch`), exit 1; the v3 side of that run was clean (epochs 0 v2, 1 and 2 v3; 0 rejected; one sink 166/166/166). Summary `class-v4-20261006-never-failed-case.json` |
| G4b | the Mac mines v4: a real Metal miner across a v4 boundary through the miner's `--prepare-packs` flow, and the app passes that flag to the Metal worker | the prepare lines on both sides, the worker's `prepared` line for the v4 pack, found or accepted blocks on v4, no `need` or mismatch line, no exit 42 or 44; the app's `miner_args` pushes `--prepare-packs` for every worker | GREEN. Run 3 (16:02:37 to 16:07:58Z, `--metal proto-metal/igneum-bench --genesis-bits 0x1e010000 --secs 480 --activation 150 --v3-activation 60`; node 0's miner is `igneum-miner --worker igneum-bench --prepare-packs <dir> --exit-on-seed-change`, the app's own shape; `igneum-bench` built from this tree at a522d04's `main.swift`, sha256 30c70754...; CPU miners on nodes 1 and 2): 5 PREPARE lines, each 5 to 6 DAA before its boundary, two `class v3` (DAA 60, 120) and three `class v4` with the pack directory and `class=v4 era=<hex>` (DAA 180, 240, 300); the worker's `prepared` lines for the three v4 packs: `prepared ca4245b5... 430.2 program 193.9 dataset 236.2 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1` (the first, the v4 day built from the pack's memhard.metal on the GPU: 1.4 ms cache fill, 70.1 ms build), then 138.6 and 175.3 ms with the day resident (program compile alone, the 256-instruction shadow block in the kernel text; a v3 prepare was 61.7 ms); every swap `swapped with no pause` at DAA 60, 120, 180, 240, 300 (prepared 2 to 5 s earlier); 301 blocks accepted by the Metal miner, 123 after the switch, every one re-checked on the CPU: `mismatched=0`; `need` 0; no class or era mismatch line, no `PACK OUT OF DATE`, no prepare-failed, no exit 42 or 44 (0 such lines in the miner's log); the chain 182 / 122 blocks across DAA 180 (304), rejected 0/0/0 and 0/0/0, one sink `4e1aa3fbf1f4a54b` on all three at 303/303/303, 3 of 3 v4 switch lines; program ids e3 v4 `2bcecca6f867c3b5`, e4 `21b78e63fc0e9251`, e5 `3855570a5ba12fc9` agreed by the Metal miner and the CPU miners; the Metal miner's STATUS at 309 s: 18.58 MH/s wall, 293 accepted, 0 rejected (a rate under a load average of 5 to 12 with the CPU miners and other agents' builds beside it: an order of magnitude, not a measurement). The app side: `app/igneum-app/src/engine.rs` `miner_args` pushes `--prepare-packs` for every worker (line 1478, the 0.3.11 fix; unit test `every_worker_gets_the_prepare_directory_in_the_platform_form`), confirmed unchanged on ca3-coord 3213ee9. Summary `class-v4-20261006-metal.json` |
| G5 | the PC-built Windows workers and the Mac workers from the same commit | every sha256 listed; the two Windows workers carry the resource block; no packaging, no DMG, no manifest | GREEN, with one gap named: the build job builds the node and the app only (`push-build-inputs.sh` writes two build units, `jobbuild.rs` has no worker unit), so the Windows workers were cross-built on the Mac from the committed tree a522d04 with `proto-cuda/nvrtc/build-windows.sh` under the build lock (mingw-w64, static, windres resource block), as 0.3.11's G5 row was: igneum-worker-cuda.exe e563126ed1d8ab8f6803ead89acca5b6fcb03203e6b87b70e0aec85e8e1046e4 (1,536,512 bytes), igneum-worker-opencl.exe 7fce1249d443aaf73a29b7474c1ef4084f3529b5bec3a2dc6bb97e624561a5e8 (478,208), `verify-exe.py`: both carry the coin icon and the version block (a first build of the same sources before the commit gave 26400edf... and 438e608a...: mingw stamps a link time, so the pair is not bit-reproducible; the sizes match the 0.3.11 pair, 1,536,512 and 478,208, and the sha256s differ from 0.3.11's 2b3b8c92... and edc4a75d...: the generator-4 rule is in them). The Mac worker from the same tree: igneum-bench 30c70754097dafbc3144a0c0f192526a0638089e5dde49ce46abb7a948dceade (665,128 bytes, built 16:47Z from `main.swift` as committed in a522d04, the binary G4b mined with). The same build job's (build-20261006-155958) node and app outputs from fork 5f7e0543, every sha256 verified against the PC's lines on fetch: igneumd.exe 140be25e486fe1634500b6e66eedfaca9fa6561d652f3e219b5a7a2bc377e766 (51,752,448), igneum-miner.exe ceb1e691ad57f58d09d5a0ff4cb5658aea0604d23ceb90b6a847a2c36a0bf81c (11,118,080), igneum-app.exe bfa3b871f17b69fd0c0c01a6f9fcac8964efb54dc10fd25db13422ba8f96d51f (3,065,344, PE and resource block ok); Linux igneumd d44b735869b129401d68f18786b4a5f16c22d00b682bd41a8e5022a01f7d7527 (49,603,112), igneum-miner a7985111d4972f4b67a9409b831a8c7af746508ebb3b19fc927aaddcfa2e2413, igneum-app 222f30c00bb9725f3127452f3d52403b380e25abaf9fba5c925839574cf1dabc; kept under the job's --out dir with --no-place. No packaging, no DMG, no manifest |
| G6 | the node change on a fork branch from the current release tip with suites green on PC 2, with the igneum-pow feature | the build job id and its SUMMARY line; the v4 engine test named in the output | GREEN. PC 2 job `build-20261006-155958` (fork ca3-v4-node 5f7e0543, main a522d04; published 15:59:58Z after the clear file and under the mkdir lock, taken 15:58:50Z, released 16:08:06Z after the closing report; the installed app untouched, the prover left on), `node tools/build-job.mjs run --target 1ccfe586 --node vendor/igneum-node-ca3v4 --node-tests "kaspa-consensus kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows" --app-tests igneum-app --no-place`: `done exit 0 after 392 s: node ca3-v4-node 5f7e0543 app 0.3.11: every stage ok; 9 files uploaded (46 MB); 6 min of 40`; the test stage 50 s, exit 0 on both units: kaspa-consensus 98 passed (3 ignored; `ban_is_decided_by_the_carrying_block_so_nodes_agree_on_every_voter_list ... ok`, so 2.0's flake did not recur and no re-run was needed), kaspa-consensus-core 109 + 7 (`override_params_carry_the_program_class_v4_activation ... ok`), igneum-exec (its per-crate line is not in the relay's upload of the report, which keeps the stage's tail; the unit exited 0, so it passed), kaspa-pow 15 (`program_class_v3_seeds_hash_their_own_program_over_their_own_cache ... ok` and `program_class_v4_seeds_hash_the_shadow_program_over_the_v3_day_cache ... ok`, both `#[cfg(feature = "igneum-pow")]`), igneum-miner (the same: not in the upload, in the exit-0 unit), kaspa-p2p-flows 33, igneum-app 112 + 26 + 8. The feature: `build-job.mjs` and `push-build-inputs.sh` carry no `--features` for the test units (the manifest's `tests` entries are `dir` and `packages` only; the build units have `kaspad/igneum-pow`), and the PC's `jobbuild.rs` runs `cargo test --release -p ... ` in ONE invocation per unit, where `igneum-miner` depends on `kaspa-pow` with `features = ["igneum-pow"]`, so cargo's feature unification turns the feature on for kaspa-pow's test target in that invocation: the two engine tests above ran on PC 2 with the feature, named in the report, which is the evidence main asked for. The same two suites also pass on the Mac with the flag explicit (`cargo test --release -p kaspa-pow --features igneum-pow`: 15; `-p kaspa-consensus-core`: 109 + 7). What the tool lacks: a `features` key on a test unit; not added here (a change to the PC's installed app would be needed to honour it) |
Summary files in this directory: `class-v4-20261006-1553Z-cpu.json` (G4 run 1), `class-v4-20261006-never-failed-case.json` (G4 run 2, the failed case), `class-v4-20261006-metal.json` (G4b).

View file

@ -0,0 +1,77 @@
# Counter ASIC 3.0: the node side (program class v4 as a second height switch)
6 October 2026, worker "ca3-v4-node". Branches: `ca3-v4-node` (main repository: igneum-pow, the workers, the fast-time gate) and `ca3-v4-node` (the fork, from the 0.3.13 tip bb43e9a8). The 2.0 twin of `docs/plans/counter-asic-2-node.md`: every piece of its section 1 has its v4 twin below; v2 and v3 are byte for byte what they were (the pinned packs, `igneum-pow/tests/packs.rs`). The class: `mx8+sh256x27` (`docs/analysis/latency-shadow-2026-10-06.md`), gated here as THE class v4 candidate on the project lead's word of 16:50 UTC. Nothing is published; the Mac's app is untouched (its miner stays paused); the live devnet is untouched. Gate rows: `docs/plans/counter-asic-3-gate/node-gates.md`.
## 1. What changed, where
| Piece | What |
|---|---|
| igneum-pow `generator.rs` | `ProgramClass::V4`, `V4_CLASS = LoadClass { shadow: Some(ShadowClass { instrs: 256, reps: 27 }), ..V3_CLASS }` (name `mx8+sh256x27`), `GENERATOR_VERSION_V4 = 4`, `generate_era_generator` (the era draw of class v3 over `V4_CLASS`, generator 4 stamped) used by `generate_from_seed_bytes_program_class` for `(V4, Some(era))`; `ProgramClass::{load_class, generator_version, from_generator, name, parse}` take v4; `ProgramClass::has_era` (v3 and v4); a v4 program's id is `program_id(4, seed, attempt)` (`Program::program_id`), its class `Program::program_class` = V4 from generator 4. Tests: `program_classes` (v4 round trip), new `program_class_v4_is_class_v3_with_the_shadow_block` (the base instructions, attempt and era draw equal class v3's of the same seed and era; shadow 256 x 27, 55,296 per hash; the id differs; the hash differs; v2's id `bcc1248b10cc90f2` unchanged) |
| igneum-pow `lib.rs` | exports `GENERATOR_VERSION_V4`, `V4_CLASS` |
| igneum-pow `emit.rs` | program.h: `IGNEUM_GENERATOR 4`, `IGNEUM_PROGRAM_CLASS "v4"`, `IGNEUM_ERA_SEED_HEX` (the v3 comment lines are byte for byte the 0.3.11 ones, so the pinned v3 packs do not change); the shadow lines (`IGNEUM_SHADOW_INSTRS`, `IGNEUM_SHADOW_REPS`, the `for (sh ...)` block in every kernel body) come from the class as before; program.json `"program_class": "v4"` |
| igneum-pow `packcheck.rs` | `verify_pack_texts_chain` accepts generator 2, 3 or 4 (`PackFault::WrongClass` otherwise, "2, 3 or 4" in the message); the era rule applies to every class with `has_era` (v3 and v4); test `program_class_and_era_are_checked` extended (a v4 pack with its era and shadow lines passes when v4 is wanted, is refused when v2 or v3 is wanted; a v3 or v2 pack is refused when v4 is wanted; the v4 era mismatch and the missing-era cases; the v3 header unchanged) |
| igneum-pow `main.rs` | `--program-class v2|v3|v4` |
| `proto-cuda/nvrtc/packfile.h` | `pf_load` accepts generator 2, 3 or 4 and names the class from it (`"v4"`); `pf_pack_class_ok` applies the era rule to every class that is not v2; `pf_class_token` unchanged (reads `class=v4`). `emu/packfile-test.c`: 7 new checks (a generator 4 pack loads as v4 with its era; v3 refuses it; it refuses another era; v4 refuses a v3 pack; the `class=v4` token; a v3 class line on a generator 4 pack is refused); `emu/packfile-test.sh` 0 failures on the Mac |
| `proto-cuda/nvrtc/worker.cpp`, `proto-opencl/host.c` | no text change: both read the class and era through `packfile.h` (`pairIsClass`, `takeClassTokens`), so a pair's identity carries `v4` and its era, a v4 job against a resident v3 pair of the same seeds answers `need` and `program class mismatch`, and the prepared v4 pair wins; `host.c` compiles clean (`cc -fsyntax-only`) against the new header |
| `proto-metal/main.swift` | `isPackClass` (v3 or v4) and `packClassOf(generator)`: `servePackProgram` and `servePackDataset` accept generator 2, 3 or 4 and refuse the rest in plain words; a `class=v4` line is compiled only from the prepared pack (its `program_bound.metal` carries the shadow block) and its day built from the pack's `memhard.metal`, keyed by (day, class, era); an inline `class=v4` job with no prepared pair answers `need` and the class-mismatch error (2.0's G4b path); a class that is not v2, v3 or v4 is refused |
| fork `consensus/core/src/igneum.rs` | `ProgramClass::V4` (generator 4, `"v4"`), `has_era`, `dataset_class` (V4 -> V3: the v4 day cache is the v3 day cache), `program_class_for_epoch_at(epoch, N4, N5, L)` with two switches (v4 from the first epoch at or above N5, else v3 from N4, else v2; N5 at or below N4 is v4 from N5), `program_class_first_epoch_at`, `program_class_v4_first_epoch_at`, the process-wide `install_program_class_v4_activation` / `program_class_v4_activation_daa` / `program_class_v4_first_epoch`; `PowEpochInfo.program_class_v4_activation_daa` (serde default never); the switch test sweeps both heights |
| fork `consensus/core/src/config/params.rs` | `program_class_v4_activation_daa` in `Params` and `OverrideParams` (never on devnet, simnet, mainnet; 0 on the testnet), `override_params`, `From<Params>`, the digest (unconditionally, right after `program_class_v3_activation_daa`), `install_program_class_v4_activation`, `program_class_v4_first_epoch`; tests: `override_params_carry_the_program_class_v4_activation`, the digest test's 15th edit, the pinned devnet digest re-pinned (section 2), `fast_time_60x_file_is_the_devnet_at_60x` (every field) |
| fork `kaspad/src/daemon.rs` | `Program class v4 from the override file: active from epoch E (DAA score N5 rounded up to the epoch boundary at L*E, epochs of L DAA)` beside the v3 line; `install_program_class_v4_activation` next to the v3 install |
| fork `consensus/pow/src/igneum.rs` | `EpochSeeds::day_key` keys the day cache on `(day, class.dataset_class())`, so a v4 epoch after a v3 one builds no new cache; `era_bytes` for every class with an era; `pow_class_of` maps V4; `build_day` builds the dataset class's day. Test `program_class_v4_seeds_hash_the_shadow_program_over_the_v3_day_cache` (one cache for v3 and v4 seeds of one day; the v4 program is generator 4, `V4_CLASS` with the era inside, the base instructions and attempt class v3's; the engine's pow equals the standalone epoch's; another era keeps the id and changes the program) |
| fork `consensus/src/consensus/mod.rs` | `get_pow_epoch_info` reports `program_class_v4_activation_daa`; the class of this and the next epoch come from `program_class_for_epoch` (both switches) |
| fork `consensus/src/pipeline/header_processor/pre_ghostdag_validation.rs`, `processes/pruning_proof/igneum_pow.rs` | unchanged: `program_class_at(daa)` answers v4 under the installed switches; the era seed is read for every class after v2 |
| fork `rpc/core/src/model/message.rs`, `rpc/grpc/core/proto/rpc.proto` (field 19), `rpc/grpc/core/src/convert/message.rs` | `RpcPowEpochInfo.program_class_v4_activation_daa` (serde default never); proto `optional uint64 programClassV4ActivationDaa = 19` so a 0.3.13 node, which sends nothing, reads as never and not as 0 (v4 from genesis); `program_class` and `next_program_class` already carry a generator number, so 4 needs no field |
| fork `igneum/miner/src/main.rs` | the v4 activation installed from the template beside the v3 one (`node program class v4 activation: ...`); `class_tokens`: a v3 or v4 epoch's job and prepare lines end with `class=<v3|v4> era=<hex>`; `seeds.txt` carries `program_class v4` and `era_seed_hex`; `write_pack_checked` checks class and era through `verify_pack_dir_chain` (generator 4 accepted); the "program and 256 MiB cache ready" line prints `class v4` and the id; `next_pair` takes the next epoch's class from the template (`next_program_class` 4 at the boundary) |
| `app/igneum-app/src/engine.rs` | confirmed, no change: `miner_args` pushes `--prepare-packs` for every worker (line 1478, since 0.3.11; the unit test `every_worker_gets_the_prepare_directory_in_the_platform_form`), so the Mac's Metal worker takes a v4 pack the way it takes a v3 one |
| `infra/fast-time/override-60x.json` | `program_class_v4_activation_daa` at never right after the v3 field. Two faults of the base file fixed on the way: the proving v1 block was in the file TWICE (two merges each appended it; the node refuses a duplicate field, so neither class harness could start a node from the file as committed), and the four 0.3.12 and 0.3.13 fields (`proving_v1_fresh_rule_daa`, `exec_restart_number`, `exec_restart_hash`, `exec_restart_trust_daa`) were missing (the fork's every-field test skips on the PCs, where the file is not beside the node). New CI check `tools/ci/override-json-check.sh` (every override file parses with no duplicate key; self-tested on a duplicate), registered in `.github/workflows/ci.yml` |
| `infra/fast-time/class-v4.mjs`, `infra/fast-time/README.md` | the G4 gate (section 3): class-v3.mjs's shape with two switches (`--v3-activation 60`, `--activation 150|never`), ports 29660, suffix 966, `/tmp/igneum-fast-time-v4`; the known-failed case is `--activation never` |
## 2. The epoch-boundary rule with two switches
One epoch has one program, so both switches key on the EPOCH: epoch `e` is v4 when `L * e >= N5`, else v3 when `L * e >= N4`, else v2 (`program_class_for_epoch_at`); each height rounds UP to the next boundary and never splits an epoch. A block's class is a function of its DAA score alone (`program_class_at`).
| N4 | N5 | L | epochs | Source |
|---|---|---|---|---|
| 60 | 150 | 60 | e0 v2; e1, e2 v3; e3 (DAA 180) v4 | the gate run, section 3; the unit test |
| 60 | 181 | 60 | e3 v3; e4 (DAA 240) v4 | the unit test |
| 154,800 | 180,001 | 3,600 | e50 v3; e51 (183,600) v4 | the unit test (the devnet shape: v3 live from epoch 43, v4 at tip + 14,400 rounded up) |
| 150 | 120 | 60 | e2 v4 (N5 below N4: v4 from N5) | the unit test |
| any | never | any | the 2.0 answers, unchanged | the unit test sweeps N4 0..399 with N5 never |
| any | 0 | any | v4 from genesis (the testnet) | the unit test |
The digest: `program_class_v4_activation_daa` enters `consensus_digest` unconditionally right after the v3 field, so the digest flips the moment a binary carrying the field runs, as the v3 field did on 0.3.11. Measured on the fork (the pinned test `consensus_digest_keeps_the_0_3_5_value_until_the_fee_switch_is_set`): the devnet digest with no override file moves from `c562d70e1428c9789823cc40067623b4767f7c555ce7ff4ea11c1498f013ef6c` (0.3.11 to 0.3.13) to `3c50502174b031b9afe8aad3758aa55eb32e3ee20ffa6f9760b2e958547c0690`. Consequence: a node on this binary is refused at the handshake by every 0.3.13 node and refuses them, which is the rollout's order step 1 (every node carries the object before any node reaches the height); the cut that carries it is a one-sweep binary rollout like 0.3.11's, and the Mac's app, PC 2 and the seeds all move in that sweep, nothing earlier.
The day cache: the v4 dataset is the v3 dataset (the same mixer x8 construction, growth rule and era layout; the shadow block touches no load and no item), so the node's engine keys its day caches on `(day, dataset_class)` and a v4 epoch reuses the v3 cache across the N5 boundary: no 1 GiB rebuild on the node at the switch (the kaspa-pow test: `cached_days` 1, `cache_builds` 1 for v3 and v4 seeds of one day). The Metal worker keeps separate (day, class, era) entries, so it builds the v4 day once at the boundary from the pack's own `memhard.metal` (the same bytes; about 41 ms on the GPU by the 2.0 figure).
## 3. The gates
See `docs/plans/counter-asic-3-gate/node-gates.md` for the rows in the rollout's section 7 shape and the summary files. The results, in short:
| Gate | State | The line that decides it |
|---|---|---|
| G4 | GREEN | run 1: 3 of 3 switch lines, 181 / 128 blocks across DAA 180, ids agree on all three miners, rejected 0/0/0, one sink 308/308/308; run 2 (`--activation never`): `SUMMARY FAIL: NO v4 epoch seen`, exit 1 |
| G4b | GREEN | three `prepared ... class v4` lines (430, 139, 175 ms), every swap with no pause, 123 blocks accepted on v4, re-check mismatched 0, need 0, no mismatch, no exit 42 or 44 |
| G5 | GREEN | the two Windows workers e563126e... and 7fce1249... with the resource block, the Mac worker 30c70754..., the job's igneumd.exe 140be25e..., every sha256 listed |
| G6 | GREEN | PC 2 job build-20261006-155958, every stage ok, 392 s; kaspa-pow 15 with the v4 engine test, no flake |
## 4. Tests
| Where | What | State |
|---|---|---|
| igneum-pow `cargo test --release` | 60 unit (the v4 class test, the v4 pack checks) + 7 derive + 4 mixer + 19 packs (the pinned v2 and v3 packs byte-identical) + 7 scratch | pass (Mac, 6 Oct 2026, 15:44Z) |
| `proto-cuda/nvrtc/emu/packfile-test.sh` | 13 + 7 checks: the generator rule with 4, the v4 pack with its era, the token matcher | 0 failures (Mac) |
| `proto-opencl/host.c` | `cc -fsyntax-only` against the new packfile.h | clean (Mac) |
| `proto-metal/main.swift` | `swiftc -O` (the G4b worker) | built (Mac) |
| fork `cargo test --release -p kaspa-consensus-core` | the two-switch rounding sweep, the v4 params, the digest's 15th edit, the pinned digest, the fast-time every-field test | pass (Mac, 109 + 7, 2 ignored; PC 2 job build-20261006-155958: 109 + 7) |
| fork `cargo test --release -p kaspa-pow --features igneum-pow` | the v4 engine test beside the v3 one | pass (Mac, 15; PC 2 job: 15, the feature on through unification with igneum-miner) |
| PC 2 `build-job.mjs` suites | `kaspa-consensus kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows` + `igneum-app` | G6 row |
## 5. Unverified, and what is owed
- The AMD vendor (G1 on the RX 9070 XT) and G2 (1,000 random hashes per card re-hashed on the CPU) are the hash side's gates, not this worker's; the OpenCL worker's generator-4 rule was checked by the C test and a syntax check, not by a live worker on a v4 pack (as 2.0's rollout read before its G1).
- The Metal worker's rate in G4b (18.6 MH/s wall) was taken under a load average of 5 to 12 with CPU miners beside it: not a measurement (the measured v4 rate is the shadow analysis's 26.5 to 26.9 MH/s on packbench under the measure lock).
- The era walk for era >= 1 and the pruning-proof era witness: unchanged from 2.0 section 7 (the v4 switch adds nothing there).
- `next_pair` at an epoch boundary that is also an era boundary: the 2.0 note stands for v4 as for v3.
- Wire: `RpcPowEpochInfo` gained one Borsh field (wRPC, versioned by `GetBlockTemplateResponse`) and one optional proto field; a 0.3.13 miner against this node reads the v4 height as never (the field absent) and would key epochs on the v3 switch alone, so every miner moves in the same binary sweep as the node (the rollout order of 2.0).
- The build job has no `features` key for test units (G6 row): the feature was on through unification; adding the key needs the PC's installed app to honour it, a 0.3.14 app item.
- The 48 GiB APFS clone of the 0.3.13 target dir (`vendor/igneum-node-ca3v4/target-ca3v4`, untracked) can be deleted after the cut.

View file

@ -86,6 +86,19 @@ Chip side (approximate: the f = 1 stored-dataset chip of item 1 plus an ALU core
Consequences per tier at N = 100,000: an Apple miner loses 1.5 percent of rate and pays 16 W more (0.56x per watt, per pound unchanged); a 5090 loses 0.2 percent and goes from 350 to 431 W (0.81x per watt), so a 5090 rig pays about 23 percent more electricity for the same blocks; a pool user sees nothing; a small NVIDIA card (4060 class) binds near 100,000 by its ALU budget (model, owed); AMD holds by its budget (owed); every verifier tier is untouched (+0.17 ms per warp).
### Item 8, the RX 9070 XT rows (PC 1 job run-ca3-pc1-amd-g1-shadow-20261006, 15:46:27 to 15:56:54Z, exit 0; the 9070 XT alone through api/cards in every key form (amd:3:gfx1201 is the installed key today), confirmed by the process list, restored in finally and mining again 15 s later; the 5090 and 4070 mining beside it, the 5090 probably clock-locked at about 2,781 MHz from the aborted Ember step; AMD OpenCL 3683.0, device-event time, 2^24 per dispatch)
| N ops per hash | Pack | 9070 XT MH/s (window s) | OpenCL build ms / dataset ms | Watts | Fingerprint = the Mac's |
|---|---|---|---|---|---|
| 930 (class v3) | mx8-devnet-epoch0 | 18.92 (82), 18.92 again at the end | 41 to 51 / 74 | OWED (the ADLX helper ran, 0 samples parsed) | yes |
| 49,700 | sh256x13 | 19.31 (80) | 429 / 74 | OWED | yes |
| 49,700, 64-instruction block | sh64x52 | 19.07 (81) | 342 / 74 | OWED | yes |
| 102,100 (the candidate) | sh256x27 | 19.29 (80) | 428 / 74 | OWED | yes |
| 199,600 | sh256x53 | 19.11 (71) | 424 / 74 | OWED | yes |
| 330,700 | sh256x88 | 19.60 (53) | 418 / 70 | OWED | yes |
Reading: the AMD card never leaves the latency bound on this ladder (its ALU budget is about 650,000 ops per hash); the +2 to 4 percent with the shadow is inside its clock and noise band. Consequence per tier: an AMD RDNA 4 owner loses nothing at the candidate and nothing at 330,700; the N the project can pick stays set by the M5 Max (about 130,000) and the capped 5090 (about 210,000); the one-click AMD miner pays 0.42 s of OpenCL compile per pack at the boundary against 0.05 on class v3, under half a second; the daily build is unchanged at 74 ms. The AMD per-joule row (item 1) and MH per W stay OWED until the sampler reads watts.
### Item 6, the 5090 rows (ca3-reserve b85f0f1; job run-ca3-family-pc2-20261006, 08:42:27 to 08:43:03Z, exit 0, the card quiet, prover off and back on; nvcc 12.8 sm_120; best of 3 runs; every row bit-exact)
| Family | M5 Max, Metal (ratio to the alu chain, 881 G steps/s) | RTX 5090, CUDA (ratio to the alu chain, 7,941 G steps/s) | RX 9070 XT | Native or emulated |
@ -164,6 +177,27 @@ Judged on measurements against the six gates of `docs/plans/counter-asic-2-rollo
So: one class v4 candidate, `mx8+sh256x27`, defined and measured on the hash's own numbers, with gates G1 (AMD), G2, G3 (Metal runs), G4, G5 and G6 still to run before any publish, by the two-publish rollout of 2.0 and a `program_class_v4_activation_daa` at tip + 14,400.
### 5a. The gate run (the project lead, 16:50 UTC: "3.0 run it now"; opened 16:5x UTC)
No publish, no manifest, nothing on the live devnet; PC 2 one job at a time with the installed app untouched; the Mac's miner stays paused. Two lanes: the hash side (branch ca3-v4-hash: G1 Metal, Apple OpenCL and CUDA, G2, G3 and the verifier benchmark; one PC 2 job first) and the node side (branch ca3-v4-node and the fork worktree igneum-node-ca3v4 from release-0.3.13-node: the v4 switch through igneum-pow, the fork, the workers, the miner and the app, then G4, G4b, G6 with `--features igneum-pow`, G5; its PC 2 jobs after the hash lane's). G1 AMD rides in PC 1 job 1 on "go PC 1 AMD". Evidence lands in docs/plans/counter-asic-3-gate/.
| Gate | What | State | Evidence (full lines in `docs/plans/counter-asic-3-gate/hash-gates.md` and `node-gates.md`, JSON per run beside them) |
|---|---|---|---|
| G1 | bit-exact v4 on every vendor against the Mac reference (2^24 fingerprint, self-test) | GREEN on all three vendors | Apple (Metal and Apple OpenCL, 15:49 to 15:50Z) and NVIDIA (the 5090, PC 2 job, 15:52Z): eight packs, three harnesses, one fingerprint per pack; AMD (PC 1 job run-ca3-pc1-amd-g1-shadow-20261006, 15:46 to 15:56Z): 7 of 7 packs equal to the Mac's on gfx1201 (sh256x27 3d2e8245cc084d07) |
| G2 | the CPU verifier exact on 1,024 random hashes per card | GREEN on NVIDIA and Apple; AMD OWED (queued on PC 1 after the Ember run) | 24 runs of 1,024 of 1,024 (eight packs on the 5090, Metal, Apple OpenCL), re-hashed on the Mac with `igneum-pow hash-bound --count 1024` on the same packs; tooling item: the found lines go to a file with a count and digest (in hand) |
| G3 | the soundness suite green on the class | GREEN | the crate suite 96 of 96 (97 on the merged tree at 17:17Z, cargo 1.99); the Metal fuzz 200 of 200 and 50 of 50; Apple OpenCL 20 of 20 and 5 of 5; 4 of 4 CPU tests on the class and on the era-composed class |
| Verifier benchmark | ms per warp on one M5 Max core, v2 / x8 / v4 in one session, gate 10 ms | GREEN | 2.33 ms on the candidate (x8 2.06), about 5.8 ms on a 2019-class core by the 2.5x rule (approximate); the 2019-class core itself still unmeasured (O-1.14) |
| G4 | the fast-time 3-node network across a v4 activation, plus the known-failed case | GREEN | run 1 (15:54 to 15:59Z, class-v4.mjs, activation 150 rounded to epoch 3 at DAA 180, the v3 switch at 60): 3 of 3 switch lines, templates class 2 / 3 / 4 by epoch, 181 blocks before and 128 after DAA 180, program ids agree on all three miners and no v2 or v3 id reappears under v4, rejected 0/0/0, one sink at 308/308/308, one digest; the first v4 epoch's cache ready in 2 ms (the v3 day cache reused across the boundary); run 2 the known-failed case (the switch at never: no v4 epoch reported) |
| G4b | a real Metal miner across a v4 boundary through --prepare-packs | GREEN | run 3 (16:02 to 16:07Z, igneum-bench from the committed tree): 5 PREPARE lines 5 to 6 DAA before each boundary, three `class=v4 era=<hex>`, the worker's `prepared` lines (430 ms the first with the v4 day built on the GPU, 139 and 175 ms with the day resident; a v3 prepare 62 ms), every swap with no pause, 301 blocks accepted on the Metal miner (123 after the switch) all re-checked on the CPU mismatched 0, `need` 0, no mismatch or out-of-date line, no exit 42 or 44 |
| G5 | the PC-built Windows workers and the Mac workers from the same commit | GREEN, one gap named | the Windows workers cross-built on the Mac from a522d04 as 0.3.11's G5 did (the build job has no worker unit: a tooling gap): igneum-worker-cuda.exe e563126e... (1,536,512 bytes), igneum-worker-opencl.exe 7fce1249... (478,208), both with the resource block, mingw not bit-reproducible (a link-time stamp); the Mac igneum-bench 30c70754... from the same tree, the binary G4b mined with; the node and app from PC 2 job build-20261006-155958, every sha256 verified |
| G6 | the node suites on PC 2 with --features igneum-pow, from a fork branch off the current release tip | GREEN | fork ca3-v4-node 5f7e0543 on release-0.3.13-node; PC 2 job build-20261006-155958 (15:59 to 16:08Z under the lock, the app untouched, the prover on): every stage ok in 392 s; kaspa-consensus 98 (2.0's flake did not recur), consensus-core 109 + 7 (`override_params_carry_the_program_class_v4_activation`), kaspa-pow 15 with the v4 engine test under the feature, p2p-flows 33, igneum-app 112 + 26 + 8 |
BLOCKER before any cut (found by the hash lane, 17:0x UTC): `program_id(3, seed, attempt)` is class-independent, so all seven v4 packs carry the same program id as the v3 control of their seed (73bcbfe8ccf988f1), and a stale worker across the activation would see no id mismatch (2.0's G4 id check cannot fire on it; G4's per-epoch ids differ only because each epoch has its own seed). The fix is on the v4 seam, assigned to the node lane: a generator-4 stamp in the id so a v3 and a v4 program of one seed differ, v2 and v3 ids byte-identical, the packs re-exported (fingerprints unchanged, ids moved), G4's id assertion and the fuzz re-run. Until it lands the gate table is green on the hash and red on the cut.
The per-tier cost line of the candidate (item 8, measured): the 5090 -0.2 percent of rate at 350 to 431 W (a rig about 23 percent more electricity), the M5 Max -1.5 percent at 21 to 37 W, pool users nothing, the verifier +0.17 ms per warp; the 9070 XT and the 4070 rows land with the PC 1 jobs. Owed before the cut, besides the blocker: AMD G2 (1,024 nonces on the 9070 XT, queued on PC 1 after the Ember run), the AMD watts (the sampler fix is in; the re-run queued), the 2019-class core (O-1.14; the US laptop's CPU could answer it with a Windows igneum-pow build, a proposal), the G2 found-lines file and digest and the 200 KB report cap (tooling, in hand on ca3-v4-hash).
THE ONE LINE FOR [user]: class v4 (`mx8+sh256x27`, 100,000 ops per hash in the latency shadow) is ready for a whole-fleet one-sweep cut (the digest moves, so every node takes it in one publish) once the program-id fix passes G4's id assertion and the fuzz re-run; its cost is the 5090 at 431 W instead of 350 for 0.2 percent less rate (a rig pays about 23 percent more electricity), the M5 Max at 37 W instead of 21 for 1.5 percent, the 9070 XT no rate at all (its watts pending), the verifier +0.27 ms per warp; what it buys is the stored-dataset chip's per-joule edge over the 5090 falling from 5.6x to 2.1x at a chip core equal to the GPU's; proposed for a day when no other cut is in flight, not tonight (0.3.14 and the fleet night come first).
## 6. Decisions for the project lead
1. The public claim. "Under 2x" is true of the recompute chip per chip at the op budget and false of the stored-dataset chip per joule (item 1). Nothing on the site or in the litepaper changes from this run; the two drafts below are for the project lead's decision, with the rows that bound them.
@ -195,7 +229,7 @@ Either replaces "under 2x" on the site and in the litepaper once the project lea
| Item | Owed | Why |
|---|---|---|
| 1 | the 5090's watts at the hash alone: the Ember Tune run of 07:21 to 07:56Z produced no 5090 step (its second engine never mined; re-run pending the project lead); the 4 October PC 1 log (miner-eff record, run win-ae432dc7-20261004-164723) gives p95 290 W at 124 MH/s in the app under a 460 W limit, and the card's cap floor is 400 W, so a power cap CANNOT bind on this kernel: item 1's 326 W denominator is a peak with the prover on, the hash alone is about 290 W (2.34 microjoules at 124 MH/s), and the lever that can move the watts is the core clock, now in item 8's PC 2 job (`-lgc` steps at 2,781 / 2,472 / 2,163 / 1,854 MHz); every DRAM energy figure is a streaming figure applied to random reads; the 9070 XT rows | PC 1 not released; no chip measured |
| 1 | the 5090's watts at the hash alone: LANDED from Ember Tune run 5 (job ember-tune-pc1-5, PC 1, 15:30 to 15:38Z, elevated, nvidia-smi -pl set directly, the clock unlocked at 2,850 MHz core and 13,801 MHz memory, 75 s a step, MH/s wall from the worker's STATUS lines): 575 W cap 127.38 MH/s at 309.9 W (0.411 MH per W); 518 W 99.32 at 313.6 (a stall inside the hold); 460 W 123.11 at 312.2; 403 W 127.38 at 310.9; 400 W (the floor) 127.38 at 311.3; 64 to 65 C. The cap never binds on this hash (310 to 314 W under every limit), the power knob is flat at 0.41 MH per W, 2.44 microjoules per hash in the app on PC 1 beside two other cards; the clock caps were not measured (the watchdog ended the run at the first clock step, fixed on ember-tune 5e4ef43) and the card was left clock-locked at about 2,781 MHz until a reset (not a card under test in the PC 1 AMD jobs). The earlier state: the run of 07:21 to 07:56Z produced no 5090 step (its second engine never mined; re-run pending the project lead); the 4 October PC 1 log (miner-eff record, run win-ae432dc7-20261004-164723) gives p95 290 W at 124 MH/s in the app under a 460 W limit, and the card's cap floor is 400 W, so a power cap CANNOT bind on this kernel: item 1's 326 W denominator is a peak with the prover on, the hash alone is about 290 W (2.34 microjoules at 124 MH/s), and the lever that can move the watts is the core clock, now in item 8's PC 2 job (`-lgc` steps at 2,781 / 2,472 / 2,163 / 1,854 MHz); every DRAM energy figure is a streaming figure applied to random reads; the 9070 XT rows | PC 1 not released; no chip measured |
| 4 | the 9070 XT rate band for the detector; the coinbase card-model tag (0.3.12) so the chain-attributed band needs no fleet log; the public testnet's first-week baseline (history check 2) | PC 1 not released; a miner change |
| 5 | a rented FPGA hour (the soft-overlay reads-in-flight number is a model on cited HBM figures) | no FPGA in the fleet |
| 6 | the 9070 XT step costs | PC 1 not released |
@ -204,4 +238,4 @@ Either replaces "under 2x" on the site and in the litepaper once the project lea
| 8 | the 9070 XT and 4060-class rows (where a small card binds); the 5090 clock rows (an elevated job); the 5090 at a 575 W cap (model only); the Mac package watts (IOReport gives GPU + DRAM, Ember's 38 W approximate); the chip side's k, lane area and 28 nm scaling; the Metal fuzz, edge and stats runs and gates G2, G4 to G6 on the class; the acceptance rule's reading of the block | PC 1 not released; no elevated job; the gates are the next step on the project lead's word |
| 6 | mm8 as a chain on Apple (Metal 4 matmul2d; the Mac's Swift toolchain has no tensor API); the 5 percent rule per family with the family live (argued only); the RDNA ISA guides unread (mnemonics from LLVM's tables) | |
| 1 | the DRAM energy figures are streaming figures applied to random 32-byte reads; the GDDR7 burst and HBM3 tFAW are behind the JEDEC paywall; no chip has been built or torn down | |
| all | every AMD RDNA 4 number in this run | PC 1 is the project lead's desk today. 16:0x UTC: the RX 9070 XT is back on PC 1 (amd:gfx1201, 16,304 MiB) beside the 5090 and an RTX 4070; main has asked for the PC 1 jobs to be PREPARED, not published: (1) G1 AMD plus item 8's ladder on the 9070 XT (about 15 min, the card alone), (2) item 6's family step costs on AMD (about 3 min), (3) item 2's dr736 build and compile on AMD (about 3 min), (4) optional: the 4070 ladder (about 10 min); branch ca3-pc1-amd; published one at a time on "go PC 1 AMD" after the 0.3.13 update and the Ember table run |
| all | every AMD RDNA 4 number in this run | PC 1 is the project lead's desk today. 16:0x UTC: the RX 9070 XT is back on PC 1 (amd:gfx1201, 16,304 MiB) beside the 5090 and an RTX 4070; main has asked for the PC 1 jobs to be PREPARED, not published: (1) G1 AMD plus item 8's ladder on the 9070 XT (about 15 min, the card alone), (2) item 6's family step costs on AMD (about 3 min), (3) item 2's dr736 build and compile on AMD (about 3 min), (4) optional: the 4070 ladder (about 10 min); branch ca3-pc1-amd (1f33cc6, e054ed7, merged): the four scripts pass every CI check, the kit zip sha256 a70fce5b... verified, the AMD watts readback is igneum-gpu-telemetry.exe (ADLX board watts); the commands and the row map in tools/ca3-pc1-amd/README.md; published one at a time on "go PC 1 AMD" after the 0.3.13 update and the Ember table run, about 33 min of PC 1 in all |

View file

@ -157,14 +157,21 @@ fn wide_load_stmt(dialect: CoreDialect, d: &str, idx: &str, width: u8, src: Wide
/// The program class lines of program.h (Counter ASIC 2.0): `IGNEUM_PROGRAM_CLASS` and, when the program was drawn
/// on the chain, `IGNEUM_ERA_SEED_HEX`. Empty for every version 2 program, so the pinned packs do not change; a
/// worker reads an absent line as class v2. The generator version is `IGNEUM_GENERATOR` as before (3 for class v3).
/// worker reads an absent line as class v2. The generator version is `IGNEUM_GENERATOR` as before (3 for class v3,
/// 4 for class v4; the v3 comment lines are byte for byte the 0.3.11 ones, so the pinned v3 packs do not change).
fn program_class_header_lines(p: &Program) -> String {
if p.program_class() == ProgramClass::V2 {
return String::new();
}
let mut s = String::new();
s.push_str("// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that\n");
s.push_str("// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).\n");
if p.program_class() == ProgramClass::V4 {
s.push_str("// Program class v4 (Counter ASIC 3.0, docs/plans/counter-asic-3-node.md): generator version 4, class v3 plus the\n");
s.push_str("// latency-shadow block (IGNEUM_SHADOW_INSTRS x IGNEUM_SHADOW_REPS per iteration); a worker that runs another class\n");
s.push_str("// refuses this pack, and a job line names the class it wants (class=v4 era=<hex>).\n");
} else {
s.push_str("// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that\n");
s.push_str("// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).\n");
}
s.push_str(&format!("#define IGNEUM_PROGRAM_CLASS {}\n", jstr(p.program_class().name())));
if let Some(era) = &p.era_bytes {
s.push_str(&format!("#define IGNEUM_ERA_SEED_HEX {}\n", jstr(&hex_bytes(era))));

View file

@ -774,15 +774,21 @@ impl Default for LoadClass {
/// Generator version of a class v3 program (spec 01 section 1.4.6: `program_id(3, seed, attempt)`).
pub const GENERATOR_VERSION_V3: u32 = 3;
/// Generator version of a class v4 program (Counter ASIC 3.0, 6 October 2026, PROPOSED: `program_id(4, seed, attempt)`).
pub const GENERATOR_VERSION_V4: u32 = 4;
/// The program class of an epoch (Counter ASIC 2.0, 5 October 2026, `docs/plans/counter-asic-2-rollout.md`): one
/// height switch in the node, `program_class_v3_activation_daa`, rounded up to an epoch boundary, decides which
/// class an epoch's program is drawn from. V2 is the lottery hash as adopted on 4 October 2026, byte for byte.
/// V3 is generator version 3: its program id carries `generator = 3` and its load class is [`V3_CLASS`].
/// V4 (Counter ASIC 3.0, 6 October 2026, the candidate `mx8+sh256x27` behind `program_class_v4_activation_daa`) is
/// generator version 4: its program id carries `generator = 4` and its load class is [`V4_CLASS`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum ProgramClass {
#[default]
V2,
V3,
V4,
}
/// The load class of program class v3, decided 5 October 2026 (Counter ASIC 2.0, `docs/plans/counter-asic-2-status.md`
@ -793,6 +799,13 @@ pub enum ProgramClass {
/// [`generate_from_seed_bytes_program_class`] (`LoadClass::era(V3_CLASS, era, &V3_ALLOWED)`); here `era` is `None`.
pub const V3_CLASS: LoadClass = LoadClass { era: None, hot: None, ..LoadClass::MX8 };
/// The load class of program class v4 (Counter ASIC 3.0 item 8, `docs/analysis/latency-shadow-2026-10-06.md`, the
/// candidate of 6 October 2026, gated by `docs/plans/counter-asic-3-node.md`): class v3 plus the latency-shadow block
/// of 256 ALU instructions run 27 times per iteration ("mx8+sh256x27", 55,296 shadow instructions per hash). The
/// base program, the 16 loads, the item construction, the cache growth rule and the era draw are class v3's, draw
/// for draw, so a v4 epoch's day cache and dataset are the v3 day's. Composed with the era exactly as V3 is.
pub const V4_CLASS: LoadClass = LoadClass { shadow: Some(ShadowClass { instrs: 256, reps: 27 }), ..V3_CLASS };
/// The width set class v3's era draw chooses from: 4 bytes only (the read-width decision of 5 October 2026; the
/// draw is consumed, so widening the set at genesis keeps the derivation).
pub const V3_ALLOWED: [u8; 1] = [1];
@ -801,8 +814,22 @@ pub const V3_ALLOWED: [u8; 1] = [1];
/// over the width set `allowed`, generator 3 stamped and the era bytes recorded (`docs/plans/era-layout.md`).
/// The chain's path is this with `base = V3_CLASS` and `allowed = V3_ALLOWED`.
pub fn generate_era(seed_string: &str, seed_bytes: &[u8], base: LoadClass, era_bytes: &[u8], allowed: &[u8]) -> Program {
generate_era_generator(seed_string, seed_bytes, base, era_bytes, allowed, era_generator_of(&base))
}
/// The generator version an era program over `base` is stamped with: 4 when the base (minus any era) is
/// [`V4_CLASS`], else 3. Counter ASIC 3.0, 6 October 2026: the seven gate packs exported through the CLI's `--era`
/// path on `mx8+sh256x27` were stamped generator 3 and so carried the v3 control's program id (`program_id(3, seed,
/// attempt)` is class-independent inside a generator version); a class v4 program is generator 4 wherever it is made.
pub fn era_generator_of(base: &LoadClass) -> u32 {
if ProgramClass::of_load_class(base) == Some(ProgramClass::V4) { GENERATOR_VERSION_V4 } else { GENERATOR_VERSION_V3 }
}
/// [`generate_era`] with the generator version stamped by the caller: 3 for class v3 over [`V3_CLASS`], 4 for
/// class v4 over [`V4_CLASS`] (Counter ASIC 3.0; the shadow block of the base rides through `LoadClass::era`).
pub fn generate_era_generator(seed_string: &str, seed_bytes: &[u8], base: LoadClass, era_bytes: &[u8], allowed: &[u8], generator: u32) -> Program {
let mut p = generate_from_seed_bytes_class(seed_string, seed_bytes, LoadClass::era(base, era_bytes, allowed));
p.generator = GENERATOR_VERSION_V3;
p.generator = generator;
p.era_bytes = Some(era_bytes.to_vec());
p
}
@ -813,6 +840,7 @@ impl ProgramClass {
match self {
ProgramClass::V2 => LoadClass::V2,
ProgramClass::V3 => V3_CLASS,
ProgramClass::V4 => V4_CLASS,
}
}
@ -821,23 +849,26 @@ impl ProgramClass {
match self {
ProgramClass::V2 => GENERATOR_VERSION,
ProgramClass::V3 => GENERATOR_VERSION_V3,
ProgramClass::V4 => GENERATOR_VERSION_V4,
}
}
/// The class of a generator version: 2 and 3 are the two classes, anything else is no class this crate runs.
/// The class of a generator version: 2, 3 and 4 are the three classes, anything else is no class this crate runs.
pub fn from_generator(generator: u32) -> Option<ProgramClass> {
match generator {
GENERATOR_VERSION => Some(ProgramClass::V2),
GENERATOR_VERSION_V3 => Some(ProgramClass::V3),
GENERATOR_VERSION_V4 => Some(ProgramClass::V4),
_ => None,
}
}
/// "v2" or "v3": the `IGNEUM_PROGRAM_CLASS` string of a pack and the `class=` token of a job line.
/// "v2", "v3" or "v4": the `IGNEUM_PROGRAM_CLASS` string of a pack and the `class=` token of a job line.
pub fn name(&self) -> &'static str {
match self {
ProgramClass::V2 => "v2",
ProgramClass::V3 => "v3",
ProgramClass::V4 => "v4",
}
}
@ -845,10 +876,31 @@ impl ProgramClass {
match s.trim() {
"v2" => Some(ProgramClass::V2),
"v3" => Some(ProgramClass::V3),
"v4" => Some(ProgramClass::V4),
_ => None,
}
}
/// Whether the class reads the era seed (every class after v2: the era layout is drawn inside the class).
pub fn has_era(&self) -> bool {
*self != ProgramClass::V2
}
/// The program class whose load class `class` is, the era draw set aside: [`LoadClass::V2`] is v2, [`V3_CLASS`]
/// is v3, [`V4_CLASS`] is v4; a measurement class (a width, a derivation length, another shadow size) is none.
pub fn of_load_class(class: &LoadClass) -> Option<ProgramClass> {
let base = LoadClass { era: None, ..*class };
if base == LoadClass::V2 {
Some(ProgramClass::V2)
} else if base == V3_CLASS {
Some(ProgramClass::V3)
} else if base == V4_CLASS {
Some(ProgramClass::V4)
} else {
None
}
}
/// The generator version as a byte, for wire formats that carry the class as a number.
pub fn as_u8(&self) -> u8 {
self.generator_version() as u8
@ -957,18 +1009,23 @@ impl Program {
/// || attempt_le32`. Written into every pack so a version 1 program, or another attempt of the same seed,
/// can never be mistaken for this one.
pub fn program_id(&self) -> u64 {
if self.class.is_v2() || self.generator == GENERATOR_VERSION_V3 {
// Spec 01 section 1.4.6: a class v3 program's id is `program_id(3, seed, attempt)`; the generator version
// in the preimage separates it from every version 2 program of the same seed
if self.class.is_v2() || self.generator == GENERATOR_VERSION_V3 || self.generator == GENERATOR_VERSION_V4 {
// Spec 01 section 1.4.6: a class v3 program's id is `program_id(3, seed, attempt)`, a class v4 program's
// `program_id(4, seed, attempt)` (Counter ASIC 3.0); the generator version in the preimage separates
// them from every version 2 program of the same seed
program_id(self.generator, &self.seed, self.attempt)
} else {
program_id_class(self.generator, &self.seed, self.attempt, &self.class)
}
}
/// The program class of this program, from its generator version (3 = v3, everything else v2).
/// The program class of this program, from its generator version (3 = v3, 4 = v4, everything else v2).
pub fn program_class(&self) -> ProgramClass {
if self.generator == GENERATOR_VERSION_V3 { ProgramClass::V3 } else { ProgramClass::V2 }
match self.generator {
GENERATOR_VERSION_V3 => ProgramClass::V3,
GENERATOR_VERSION_V4 => ProgramClass::V4,
_ => ProgramClass::V2,
}
}
}
@ -1306,15 +1363,18 @@ pub fn generate_from_seed_bytes_class(seed_string: &str, seed_bytes: &[u8], clas
/// program, so its packs and its id say generator 3 (spec 01 sections 1.4.5 and 1.4.6).
pub fn generate_from_seed_bytes_program_class(seed_string: &str, seed_bytes: &[u8], class: ProgramClass, era_bytes: Option<&[u8]>) -> Program {
// Class v3 with an era seed: the era layout (docs/plans/era-layout.md) drawn from the era bytes inside V3_CLASS.
// Without era bytes (a template before the era is known) the bare V3_CLASS stands.
if let (ProgramClass::V3, Some(era)) = (class, era_bytes) {
return generate_era(seed_string, seed_bytes, V3_CLASS, era, &V3_ALLOWED);
// Class v4 (Counter ASIC 3.0): the same draw inside V4_CLASS (V3_CLASS plus the shadow block), generator 4.
// Without era bytes (a template before the era is known) the bare class stands.
match (class, era_bytes) {
(ProgramClass::V3, Some(era)) => return generate_era(seed_string, seed_bytes, V3_CLASS, era, &V3_ALLOWED),
(ProgramClass::V4, Some(era)) => return generate_era_generator(seed_string, seed_bytes, V4_CLASS, era, &V3_ALLOWED, GENERATOR_VERSION_V4),
_ => {}
}
let mut p = generate_from_seed_bytes_class(seed_string, seed_bytes, class.load_class());
p.generator = class.generator_version();
// The era is a property of class v3 programs; a version 2 program never records one, so the pinned v2 packs
// and every v2 export stay byte-identical whatever the chain reports for the era
p.era_bytes = if class == ProgramClass::V3 { era_bytes.map(|b| b.to_vec()) } else { None };
// The era is a property of class v3 and v4 programs; a version 2 program never records one, so the pinned v2
// packs and every v2 export stay byte-identical whatever the chain reports for the era
p.era_bytes = if class.has_era() { era_bytes.map(|b| b.to_vec()) } else { None };
p
}
@ -1644,16 +1704,81 @@ mod tests {
assert_eq!(v3.program_id(), program_id(GENERATOR_VERSION_V3, &v3.seed, v3.attempt));
assert_ne!(v3.program_id(), program_id(GENERATOR_VERSION, &v3.seed, v3.attempt));
assert_ne!(v3.program_id(), v2.program_id());
for c in [ProgramClass::V2, ProgramClass::V3] {
for c in [ProgramClass::V2, ProgramClass::V3, ProgramClass::V4] {
assert_eq!(ProgramClass::parse(c.name()), Some(c));
assert_eq!(ProgramClass::from_generator(c.generator_version()), Some(c));
assert_eq!(ProgramClass::from_u8(c.as_u8()), Some(c));
}
assert_eq!(ProgramClass::from_generator(1), None);
assert_eq!(ProgramClass::from_generator(4), None);
assert_eq!(ProgramClass::parse("v4"), None);
assert_eq!(ProgramClass::from_generator(5), None);
assert_eq!(ProgramClass::parse("v5"), None);
assert_eq!(ProgramClass::default(), ProgramClass::V2);
assert_eq!(ProgramClass::V2.load_class(), LoadClass::V2);
assert!(!ProgramClass::V2.has_era() && ProgramClass::V3.has_era() && ProgramClass::V4.has_era());
}
/// Counter ASIC 3.0 (6 October 2026): class v4 is class v3 with the latency-shadow block `sh256x27`, drawn after
/// the base program, so a v4 program's base instructions, attempt and era draw are the v3 program's of the same
/// seed and era, draw for draw; its generator is 4, its id `program_id(4, seed, attempt)`, its era recorded;
/// v2 and v3 are untouched.
#[test]
fn program_class_v4_is_class_v3_with_the_shadow_block() {
assert_eq!(V4_CLASS, V3_CLASS.with_shadow(256, 27));
assert_eq!(V4_CLASS.name(), "mx8+sh256x27");
assert_eq!(LoadClass::parse("mx8+sh256x27"), Some(V4_CLASS));
assert_eq!(LoadClass { shadow: None, ..V4_CLASS }, V3_CLASS, "v4 differs from v3 in the shadow alone");
assert_eq!(V4_CLASS.shadow_instrs_per_hash(), 8 * 256 * 27);
assert_eq!(ProgramClass::V4.load_class(), V4_CLASS);
assert_eq!(ProgramClass::V4.generator_version(), GENERATOR_VERSION_V4);
let era = [7u8; 32];
let v3 = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V3, Some(&era));
let v4 = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era));
assert_eq!(v4.generator, GENERATOR_VERSION_V4);
assert_eq!(v4.program_class(), ProgramClass::V4);
assert_eq!(v4.era_bytes.as_deref(), Some(&era[..]));
assert_eq!(v4.instrs, v3.instrs, "the base program is class v3's, draw for draw");
assert_eq!(v4.attempt, v3.attempt);
assert_eq!(v4.seed, v3.seed);
assert!(v3.shadow.is_empty() && !v3.has_shadow());
assert_eq!(v4.shadow.len(), 256);
assert_eq!(v4.shadow_reps(), 27);
assert_eq!(v4.shadow_instrs_per_hash(), 55_296);
assert_eq!(LoadClass { era: None, ..v4.class }, V4_CLASS, "the era rides inside V4_CLASS");
assert_eq!(v4.class.era, v3.class.era, "the same era draw as class v3");
assert_eq!(v4.class, LoadClass::era(V4_CLASS, &era, &V3_ALLOWED));
assert!(check(&v4).is_ok());
assert_eq!(v4.program_id(), program_id(GENERATOR_VERSION_V4, &v4.seed, v4.attempt));
assert_ne!(v4.program_id(), v3.program_id());
assert_ne!(v4.program_id(), program_id(GENERATOR_VERSION, &v4.seed, v4.attempt));
// the CLI's `--era` path (generate_era over a base class) stamps 4 on V4_CLASS and 3 on anything else, so a
// pack exported with `--class mx8+sh256x27 --era` is a class v4 pack with the v4 id (the 6 October trap:
// the seven gate packs stamped 3 carried the v3 control's id)
let via_era = generate_era("igneum-genesis", b"igneum-genesis", V4_CLASS, &era, &V3_ALLOWED);
assert_eq!(via_era, v4, "the --era path and the chain path make the same v4 program");
assert_eq!(via_era.generator, GENERATOR_VERSION_V4);
assert_eq!(generate_era("igneum-genesis", b"igneum-genesis", V3_CLASS, &era, &V3_ALLOWED), v3);
assert_eq!(generate_era("igneum-genesis", b"igneum-genesis", LoadClass::MX8.with_shadow(256, 13), &era, &V3_ALLOWED).generator, GENERATOR_VERSION_V3, "a measurement class stays generator 3");
assert_eq!(ProgramClass::of_load_class(&v4.class), Some(ProgramClass::V4));
assert_eq!(ProgramClass::of_load_class(&v3.class), Some(ProgramClass::V3));
assert_eq!(ProgramClass::of_load_class(&LoadClass::V2), Some(ProgramClass::V2));
assert_eq!(ProgramClass::of_load_class(&LoadClass::MX8.with_shadow(256, 13)), None);
assert_eq!(ProgramClass::of_load_class(&LoadClass::MX4), None);
// without an era (a template before the era is known) the bare V4_CLASS stands, generator 4
let bare = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, None);
assert_eq!(bare.class, V4_CLASS);
assert_eq!(bare.generator, GENERATOR_VERSION_V4);
assert_eq!(bare.era_bytes, None);
// the block changes the hash over the same dataset; the same seed and era derive the same block again
let ds = crate::verify::DatasetSource::new("2026-10-06", crate::verify::DatasetMode::ClosedForm, 20);
let h3 = crate::verify::hash_warp(&v3, 0, &ds);
let h4 = crate::verify::hash_warp(&v4, 0, &ds);
assert_ne!(h3, h4);
let again = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era));
assert_eq!(again.shadow, v4.shadow);
assert_eq!(crate::verify::hash_warp(&again, 0, &ds), h4);
// v2 and v3 are byte for byte what they were (the pinned packs are diffed in tests/packs.rs)
assert_eq!(V3_CLASS.shadow, None);
assert_eq!(generate("igneum-genesis").program_id(), 0xbcc1248b10cc90f2);
}
/// Era layout: the draw is deterministic, within bounds, and the six test eras are pinned; an era program has

View file

@ -34,7 +34,7 @@ pub mod verify;
pub use bind::{block_init_words, day_bytes, pow256_from_lane, target64_from_le256};
pub use accept::{check as accept_program, AcceptReport, Reject};
pub use generator::{generate, generate_from_seed_bytes, generate_from_seed_bytes_program_class, Instr, LoadClass, Op, Program, ProgramClass, GENERATOR_VERSION, GENERATOR_VERSION_V3, V3_CLASS};
pub use generator::{generate, generate_from_seed_bytes, generate_from_seed_bytes_program_class, Instr, LoadClass, Op, Program, ProgramClass, GENERATOR_VERSION, GENERATOR_VERSION_V3, GENERATOR_VERSION_V4, V3_CLASS, V4_CLASS};
pub use memhard::{cache_log2_words, dataset_log2_words, days_since_genesis, growth_doublings, Cache, MemhardCpu, MixParams, Shape};
pub use seed::{fnv1a64, seed_words, SplitMix64};
pub use verify::{hash_warp, interpret_warp_init, verify_block, DatasetMode, DatasetSource, Epoch};

View file

@ -42,7 +42,7 @@ struct Args {
class: LoadClass,
/// Days since genesis for the cache growth rule of a class with `growth` (0: the genesis cache).
days: u64,
/// The program class (Counter ASIC 2.0 seam): v2 (default) or v3, which draws from V3_CLASS with generator 3.
/// The program class (Counter ASIC 2.0 seam): v2 (default), v3 (V3_CLASS, generator 3) or v4 (V4_CLASS, generator 4).
program_class: Option<ProgramClass>,
/// The era seed bytes a class v3 chain program records (`--era-hex`).
era_hex: Option<String>,
@ -98,7 +98,7 @@ fn usage() -> ! {
\x20 --class C load class: v2 (default), mx4, mx8 (class v3: mixer x8, cache growth), dr<len> (Counter ASIC 3.0 item 2: the per-day derivation program, dr736 = the x8-equivalent), w4, w16, w64, w64x4, p4,p16,p64[xN], <class>m<mult>[g]\n\
\x20 also: w4, w16, w64, w64x4, p4,p16,p64[xN], <class>m<mult>[g], <class>+sh<S>x<R> (latency-shadow block of S ALU instructions x R passes per iteration, Counter ASIC 3.0 item 8)\n\
\x20 --days N days since genesis for the cache growth rule of a class with it (default 0: the 2^26-word cache)\n\
\x20 --program-class v2|v3 the program class of the seam (v3 = generator 3 on V3_CLASS, the chain's own derivation; --era-hex records the era seed)\n\
\x20 --program-class v2|v3|v4 the program class of the seam (v3 = generator 3 on V3_CLASS, v4 = generator 4 on V4_CLASS = mx8+sh256x27, the chain's own derivation; --era-hex records the era seed)\n\
\x20 --era E era layout over --class: igneum-era-test/<n> or <n>:<64 hex> (the 32-byte era seed E_n)\n\
\x20 --era-widths 4[,16,64] the width set the era draws from, in bytes (default 4: pinned; more lets the era draw it)"
);
@ -161,7 +161,8 @@ fn parse() -> Args {
/// `Epoch::from_chain_seeds` does); the pack then carries IGNEUM_PROGRAM_CLASS "v3" and IGNEUM_ERA_SEED_HEX.
fn stamp_era(e: &mut Epoch, a: &Args) {
if let Some((_, bytes, _)) = &a.era {
e.program.generator = GENERATOR_VERSION_V3;
// generator 4 on V4_CLASS (class v4, Counter ASIC 3.0), 3 on every other era program
e.program.generator = igneum_pow::generator::era_generator_of(&e.program.class);
e.program.era_bytes = Some(bytes.clone());
}
}
@ -405,9 +406,15 @@ fn accept(a: &Args) {
fn show(a: &Args) {
let (label, bytes) = seed_bytes_of(a);
let mut p = igneum_pow::generator::generate_from_seed_bytes_class(&label, &bytes, a.class);
if let Some((_, eb, _)) = &a.era {
p.generator = GENERATOR_VERSION_V3;
// --program-class: the chain's own derivation (the era from --era-hex), as the node and the miner draw it; else
// the load class of --class, the era of --era composed over it
let era_hex_bytes = a.era_hex.as_deref().map(|h| igneum_pow::bind::unhex(h).unwrap_or_else(|| usage()));
let mut p = match a.program_class {
Some(pc) => igneum_pow::generator::generate_from_seed_bytes_program_class(&label, &bytes, pc, era_hex_bytes.as_deref()),
None => igneum_pow::generator::generate_from_seed_bytes_class(&label, &bytes, a.class),
};
if let (None, Some((_, eb, _))) = (a.program_class, &a.era) {
p.generator = igneum_pow::generator::era_generator_of(&p.class);
p.era_bytes = Some(eb.clone());
}
println!(

View file

@ -23,7 +23,7 @@ pub struct PackIdentity {
pub attempt: u32,
pub seedw: [u32; 8],
pub keyw: [u32; 8],
/// `IGNEUM_GENERATOR` (2 or 3; a pack without the line is generator 1, which no worker runs).
/// `IGNEUM_GENERATOR` (2, 3 or 4; a pack without the line is generator 1, which no worker runs).
pub generator: u32,
/// The program class the generator version names (Counter ASIC 2.0).
pub class: ProgramClass,
@ -164,9 +164,9 @@ pub fn verify_pack_texts(program_h: &str, seeds_txt: Option<&str>, want_epoch: &
verify_pack_texts_chain(program_h, seeds_txt, want_epoch, want_day, None, None)
}
/// [`verify_pack_texts`] that also demands a program class and, for class v3, the era seed the chain is on
/// (Counter ASIC 2.0, 5 October 2026). `want_class` `None` accepts either class; `want_era` `None` skips the era.
/// A pack whose `IGNEUM_GENERATOR` is neither 2 nor 3 is refused whatever is wanted.
/// [`verify_pack_texts`] that also demands a program class and, for class v3 and v4, the era seed the chain is on
/// (Counter ASIC 2.0, 5 October 2026; class v4 Counter ASIC 3.0, 6 October 2026). `want_class` `None` accepts any
/// class; `want_era` `None` skips the era. A pack whose `IGNEUM_GENERATOR` is not 2, 3 or 4 is refused whatever is wanted.
pub fn verify_pack_texts_chain(
program_h: &str,
seeds_txt: Option<&str>,
@ -177,7 +177,7 @@ pub fn verify_pack_texts_chain(
) -> Result<PackIdentity, PackFault> {
let generator = define_u32(program_h, "IGNEUM_GENERATOR").unwrap_or(1);
let Some(class) = ProgramClass::from_generator(generator) else {
return Err(PackFault::WrongClass(format!("IGNEUM_GENERATOR {generator} is not a generator version this software runs (2 or 3)")));
return Err(PackFault::WrongClass(format!("IGNEUM_GENERATOR {generator} is not a generator version this software runs (2, 3 or 4)")));
};
// IGNEUM_PROGRAM_CLASS, when present, must name the class the generator version names
if let Some(named) = define_str(program_h, "IGNEUM_PROGRAM_CLASS") {
@ -186,17 +186,31 @@ pub fn verify_pack_texts_chain(
}
}
let era_hex = define_str(program_h, "IGNEUM_ERA_SEED_HEX").map(|h| h.to_ascii_lowercase());
// Counter ASIC 3.0 (6 October 2026): the shadow block is the mark of class v4, so a generator 4 pack carries
// IGNEUM_SHADOW_INSTRS and a generator 3 pack does not; a pack exported as class v4 but stamped generator 3 (the
// seven gate packs of 6 October, which carried the v3 control's program id because `program_id(3, ..)` is
// class-independent) is refused here instead of mining as class v3 under the wrong id, and a generator 4 pack
// without the block is no v4 pack. A generator 2 pack with a shadow (the measurement ladder of
// proto-cuda/packs-ca3-shadow) carries a class-bearing id and stays loadable.
let shadow = define_u32(program_h, "IGNEUM_SHADOW_INSTRS").unwrap_or(0);
match (class, shadow > 0) {
(ProgramClass::V4, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack".into())),
(ProgramClass::V3, true) => {
return Err(PackFault::Disagree(format!("IGNEUM_GENERATOR 3 (class v3) with a shadow block (IGNEUM_SHADOW_INSTRS {shadow}): a class v4 program is generator 4 (export the pack as class v4)")))
}
_ => {}
}
if let Some(want) = want_class {
if want != class {
return Err(PackFault::WrongClass(format!("the pack is program class {} (generator {generator}), the chain is on class {}", class.name(), want.name())));
}
}
if let (Some(want), ProgramClass::V3) = (want_era, class) {
if let (Some(want), true) = (want_era, class.has_era()) {
let want_hex = hex(want);
match &era_hex {
Some(h) if *h == want_hex => {}
Some(h) => return Err(PackFault::WrongClass(format!("the pack's era seed {} is not the era seed {} the job names", short(h), short(&want_hex)))),
None => return Err(PackFault::WrongClass("a class v3 pack without IGNEUM_ERA_SEED_HEX; the job names an era seed".into())),
None => return Err(PackFault::WrongClass(format!("a class {} pack without IGNEUM_ERA_SEED_HEX; the job names an era seed", class.name()))),
}
}
let seedw = define_words(program_h, "IGNEUM_SEEDW_INIT").ok_or_else(|| PackFault::Unreadable("program.h has no IGNEUM_SEEDW_INIT with 8 words".into()))?;
@ -393,5 +407,37 @@ mod tests {
// a class line that contradicts the generator
let bad = h3.replace("#define IGNEUM_PROGRAM_CLASS \"v3\"\n", "#define IGNEUM_PROGRAM_CLASS \"v2\"\n");
assert!(matches!(verify_pack_texts(&bad, None, &e, &d), Err(PackFault::Disagree(_))));
// Counter ASIC 3.0: a class v4 chain pack carries generator 4, the class line and the era seed; it is refused
// when v3 (or v2) is wanted, and a v3 pack is refused when v4 is wanted; the era rule applies to v4 as to v3
let v4 = Epoch::from_chain_seeds(&e, &d, Some(&era), ProgramClass::V4, "class test");
let h4 = program_header(&v4.program, "test day", &v4.dataset);
assert!(h4.contains("#define IGNEUM_GENERATOR 4\n"));
assert!(h4.contains("#define IGNEUM_PROGRAM_CLASS \"v4\"\n"));
assert!(h4.contains(&format!("#define IGNEUM_ERA_SEED_HEX \"{}\"\n", hex(&era))));
assert!(h4.contains("#define IGNEUM_SHADOW_INSTRS 256\n") && h4.contains("#define IGNEUM_SHADOW_REPS 27\n"), "{h4}");
let id = verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V4), Some(&era)).unwrap();
assert_eq!((id.generator, id.class, id.era_hex.as_deref()), (4, ProgramClass::V4, Some(hex(&era).as_str())));
assert_eq!(id.attempt, v3.program.attempt, "the v4 attempt is the v3 attempt of the same seed");
assert!(verify_pack_texts(&h4, None, &e, &d).is_ok(), "no class wanted: any class passes");
assert!(matches!(verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V3), None), Err(PackFault::WrongClass(_))));
assert!(matches!(verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V2), None), Err(PackFault::WrongClass(_))));
assert!(matches!(verify_pack_texts_chain(&h3, None, &e, &d, Some(ProgramClass::V4), None), Err(PackFault::WrongClass(_))));
assert!(matches!(verify_pack_texts_chain(&h2, None, &e, &d, Some(ProgramClass::V4), None), Err(PackFault::WrongClass(_))));
let err = verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V4), Some(&[1u8; 32])).unwrap_err();
assert!(err.to_string().contains("era seed"), "{err}");
let no_era = h4.replace(&format!("#define IGNEUM_ERA_SEED_HEX \"{}\"\n", hex(&era)), "");
let err = verify_pack_texts_chain(&no_era, None, &e, &d, Some(ProgramClass::V4), Some(&era)).unwrap_err();
assert!(err.to_string().contains("class v4 pack without IGNEUM_ERA_SEED_HEX"), "{err}");
// the v3 pack of the same seeds is unchanged by the v4 class existing
assert_eq!(program_header(&v3.program, "test day", &v3.dataset), h3);
// the 6 October trap: a v4 program stamped generator 3 (the CLI's old --era path) is refused for its shadow
// block, whatever class is wanted; and a generator 4 header without the block is refused too
let stamped3 = h4.replace("#define IGNEUM_GENERATOR 4\n", "#define IGNEUM_GENERATOR 3\n").replace("#define IGNEUM_PROGRAM_CLASS \"v4\"\n", "#define IGNEUM_PROGRAM_CLASS \"v3\"\n");
let err = verify_pack_texts(&stamped3, None, &e, &d).unwrap_err();
assert!(matches!(err, PackFault::Disagree(_)) && err.to_string().contains("shadow block"), "{err}");
assert!(verify_pack_texts_chain(&stamped3, None, &e, &d, Some(ProgramClass::V3), Some(&era)).is_err());
let no_shadow = h4.replace("#define IGNEUM_SHADOW_INSTRS 256\n", "");
let err = verify_pack_texts(&no_shadow, None, &e, &d).unwrap_err();
assert!(matches!(err, PackFault::Disagree(_)) && err.to_string().contains("without IGNEUM_SHADOW_INSTRS"), "{err}");
}
}

View file

@ -30,6 +30,12 @@ every node, `program_class_v3_activation_daa` a few epochs ahead; reports blocks
program id and class before and after, rejected blocks, forks, and every node's switch line; see
`docs/plans/counter-asic-2-node.md`).
Class v4 switch gate (Counter ASIC 3.0 G4): `node infra/fast-time/class-v4.mjs` (the same shape on ports 29660 and up,
`igneum-devnet-966`, data `/tmp/igneum-fast-time-v4`, `program_class_v3_activation_daa` at 60 and
`program_class_v4_activation_daa` at 150 by default, so one run crosses v2, v3 and v4; `--activation never` is the
known-failed case, which must report FAIL with no v4 epoch; `--metal <igneum-bench>` puts a real Metal miner on node 0
for G4b; see `docs/plans/counter-asic-3-node.md`).
Miners: `igneum-miner` follows the epoch length and lead its node reports in every template (`pow_epoch`), no flag.
The dataset day is not in the template, so a real-hash miner on a fast-time network takes `IGNEUM_POW_DAY_MS=1440000`
in its environment (the node reads it from the file). The environment variables `IGNEUM_POW_EPOCH_BLOCKS`,
@ -62,6 +68,8 @@ Time parameters, divided by 60 (devnet value, 60x value):
| `proving_v1_segment_blocks`, `proving_v1_aggregator_share_bps` | 8, 1,000 | 8, 1,000 | a block count and a share |
| `proving_v1_unproven_daa` | 600 | 10 | a DAA clock (the unproven allowance), divided by 60; the proving agent confirms the value |
| `program_class_v3_activation_daa` | never | never | a height, not a clock: the lottery hash draws programs from class v3 (Counter ASIC 2.0, 5 Oct 2026) from the first EPOCH whose start is at or above this DAA score (rounded up to an epoch boundary: at 60 DAA per epoch, 150 means epoch 3 at DAA 180); `infra/fast-time/class-v3.mjs` sets it a few epochs ahead in its merged file |
| `program_class_v4_activation_daa` | never | never | a height, not a clock: the lottery hash draws programs from class v4 (Counter ASIC 3.0, 6 Oct 2026: class v3 plus the latency-shadow block) from the first EPOCH whose start is at or above this DAA score, rounded up like the v3 switch; `infra/fast-time/class-v4.mjs` sets it a few epochs ahead in its merged file |
| `proving_v1_fresh_rule_daa`, `exec_restart_number`, `exec_restart_trust_daa`, `exec_restart_hash` | never, never, never, "" | the same | heights and a hash, not clocks (the 0.3.12 and 0.3.13 switches); present so the fork's every-field test (`fast_time_60x_file_is_the_devnet_at_60x`) holds; added 6 Oct 2026 with the class v4 field |
Unchanged, and why:

View file

@ -0,0 +1,331 @@
#!/usr/bin/env node
// Counter ASIC 3.0 gate G4 (docs/plans/counter-asic-3-node.md; the 2.0 shape: docs/plans/counter-asic-2-rollout.md
// section 7): the fast-time 3-node network mining across a program class v4 activation. A private network on
// 127.0.0.1 ports 29660 and up, data under /tmp/igneum-fast-time-v4, network id igneum-devnet-966, every node on
// infra/fast-time/override-60x.json merged with a CPU genesis difficulty (genesis_bits 0x1f010000, 2^16 hashes per
// block, as sim/difficulty/testnet_v2.py), `program_class_v3_activation_daa` at --v3-activation (default 60: epoch 1,
// so the run crosses v2 -> v3 -> v4 and the two switches are both exercised) and `program_class_v4_activation_daa`
// at --activation (default 150: inside epoch 2 at 60 DAA per epoch, so the switch rounds UP to epoch 3 at DAA 180,
// the boundary rule under test). One real CPU miner per node (igneum-miner --engine igneum-pow, 1 thread) follows
// its node's templates, so every block of the run is a real lottery-hash solution and every node verifies every
// block of the other two under the class of its epoch.
//
// Reports, from the nodes' RPC and the logs: blocks on each side of the v4 boundary, the program class and id of
// every epoch (before and after), rejected blocks (the miners' submit answers and the nodes' "PoW rejected" lines),
// forks (every node's sink, selected tip and block count at the end), and every node's two switch lines. Exit 0 when
// every check passes. Never touches the live devnet (26610/26611, 26640/26641, 28640), the class-v3 harness (29600)
// or the simnet.mjs ports.
//
// The known-failed case (the gate rule: a gate is trusted only after it fires on one finished and one failed case):
// `--activation never` leaves the v4 switch at never; the harness must then report FAIL with no v4 epoch seen.
//
// node infra/fast-time/class-v4.mjs [--secs 420] [--activation 150|never] [--v3-activation 60|never] [--epochs-after 2]
// [--threads 1] [--metal <igneum-bench>] [--genesis-bits 0x1f010000]
// IGNEUMD and IGNEUM_MINER name the binaries (default: the ca3 fork worktree's target-ca3v4/release).
// --metal: node 0's miner is a real Metal GPU worker (igneum-miner --worker <igneum-bench> --prepare-packs <dir>
// --exit-on-seed-change, the app's own shape), so the Metal worker's class v4 path (the prepare line with the pack
// directory and the class and era tokens, the pack-built program and day) mines across the boundary (gate G4b);
// the report then adds the miner's PREPARE lines, the worker's prepared lines, any need / mismatch / refusal line,
// the accepted blocks on each side and the swap time. --genesis-bits raises the CPU difficulty for a GPU miner
// (0x1e010000 = 2^24 hashes per block: under a second on an M5 Max; the CPU miners on nodes 1 and 2 then verify
// and rarely find).
import { spawn, spawnSync } from 'node:child_process';
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_CA3_BIN || `${ROOT}vendor/igneum-node-ca3v4/target-ca3v4/release`;
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
// the igneum-pow CLI, for the program-id assertion (the same-seed v3 and v4 ids of every v4 epoch)
const IGNEUM_POW = process.env.IGNEUM_POW || `${ROOT}igneum-pow/target/release/igneum-pow`;
const TMP = '/tmp/igneum-fast-time-v4';
const BASE = 29660, SUFFIX = 966;
const NEVER = '18446744073709551615';
const args = process.argv.slice(2);
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? +args[i + 1] : dflt; };
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
// an activation flag: a DAA score, or "never" (the known-failed case for the v4 switch; a v2-only chain for the v3 one)
const actflag = (name, dflt) => { const v = sflag(name); if (v == null) return dflt; return v === 'never' ? null : +v; };
const GENESIS_BITS = flag('genesis-bits', 0x1f010000);
const METAL = sflag('metal');
const SECS = flag('secs', 420);
const ACTIVATION = actflag('activation', 150); // class v4 (N5); null = never
const V3_ACTIVATION = actflag('v3-activation', 60); // class v3 (N4); null = never
const EPOCHS_AFTER = flag('epochs-after', 2);
// The program-id assertion (6 October 2026, the hash lane's finding: program_id(3, seed, attempt) is class-independent, so
// a v4 pack stamped generator 3 carried the v3 id and no id check could fire): every v4 epoch's id, as the three miners
// print it, must equal the CLI's class v4 id of that seed and era and DIFFER from the CLI's class v3 id of the same seed
// and era. `--id-check-against v4` is the assertion's own known-failed case: the "must differ" side is then the v4 id
// itself, so the check must read false.
const ID_AGAINST = sflag('id-check-against') || 'v3';
const THREADS = flag('threads', 1);
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, ...(METAL ? [METAL] : [])]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
// The file is merged as TEXT, never through JSON.parse: a `never` height is 18446744073709551615, which JavaScript
// rounds to 1.8446744073709552e+19 and the node refuses ("expected u64"; the first gate run, 5 October 2026 21:30Z).
// The merged fields are appended after the file's last field; a field already in the file is removed first.
const baseText = readFileSync(FILE, 'utf8');
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
const EPOCH = field('pow_epoch_blocks');
const DAY_MS = field('pow_day_ms');
const FIRST_V4_EPOCH = ACTIVATION == null ? null : Math.ceil(ACTIVATION / EPOCH);
const FIRST_V3_EPOCH = V3_ACTIVATION == null ? null : Math.ceil(V3_ACTIVATION / EPOCH);
const BOUNDARY = FIRST_V4_EPOCH == null ? Infinity : FIRST_V4_EPOCH * EPOCH;
const override = `${TMP}/override.json`;
export function mergeOverrideText(text, fields) {
let out = text;
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
// a string of digits is written raw (a never height is the u64 max, beyond a JavaScript number); other strings quoted
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 two heights as digit text (the file's own two fields, both at never, are removed first by the merge)
const asText = (v) => v == null ? NEVER : String(v);
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(ACTIVATION) }));
log(`v3 activation ${V3_ACTIVATION ?? 'never'} (first v3 epoch ${FIRST_V3_EPOCH ?? 'none'}), v4 activation ${ACTIVATION ?? 'never'} at ${EPOCH} DAA per epoch: the first v4 epoch is ${FIRST_V4_EPOCH ?? 'none'} (DAA ${Number.isFinite(BOUNDARY) ? BOUNDARY : 'never'}); run ${SECS} s, CPU genesis bits 0x${GENESIS_BITS.toString(16)}`);
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(1200);
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}`);
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;
}
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 { } }
}
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); }); // a thrown start leaves no node behind
const minerName = (i) => (METAL && i === 0) ? 'metal0' : `cpu${i}`;
const minerLog = (i) => { try { return readFileSync(`${TMP}/${minerName(i)}.log`, 'utf8').split('\n'); } catch { return []; } };
const V4_LINE = /Program class v4 from the override file/, V3_LINE = /Program class v3 from the override file/;
const tail = (l, what) => l.replace(new RegExp(`^.*?(Program class ${what})`), '$1');
const t0 = Date.now();
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
const n0 = await new Node(0).start();
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start();
const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`]).start(); // --connect takes one address
const nodes = [n0, n1, n2];
for (const n of nodes) {
const v3 = n.grepLog(V3_LINE)[0], v4 = n.grepLog(V4_LINE)[0];
log(`n${n.i} switch lines: ${v3 ? tail(v3, 'v3') : '(no v3 line)'} | ${v4 ? tail(v4, 'v4') : '(no v4 line)'}`);
}
log(`n0 PoW schedule: ${n0.grepLog(/PoW schedule from the override file/).map(l => l.replace(/^.*?(PoW schedule)/, '$1')).join(' | ') || '(no line)'}`);
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
// one real CPU miner per node, 1 thread, 2^16 expected hashes per block at genesis; with --metal, node 0's miner drives
// the Metal worker the way the app does (the pack for every prepared pair under packs/prepare, exit 42/44 on a refusal)
const PACKS = `${TMP}/packs/prepare`;
mkdirSync(PACKS, { recursive: true });
nodes.forEach((n, i) => {
if (METAL && i === 0) {
miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS), 'metal0', '--engine', 'igneum-pow', '--worker', METAL, '--prepare-packs', PACKS, '--exit-on-seed-change', '--payout-label', 'metal0', '--status-secs', '30', '--no-vote'], 'metal0', { IGNEUM_POW_DAY_MS: String(DAY_MS) });
log(`n0 miner: Metal worker ${METAL}, packs under ${PACKS}`);
} else {
miner(CPU_MINER, ['mine', n.grpc, String(THREADS), String(SECS), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) });
}
});
const pay = devAddress('fast-time-v4');
const epochs = new Map(); // epoch index -> { class, firstSeenDaa, at, eraSeed }
let firstV4 = null, firstV3 = null, lastEpoch = -1, lastReport = 0, lastDaa = 0, switchEnd = null;
const samples = [];
while (Date.now() - t0 < SECS * 1000) {
await sleep(1000);
let daa = null, epoch = null, cls = null, nextCls = null, era = null, eraSeed = null, act3 = null, act4 = null;
try {
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;
era = pe.eraIndex; eraSeed = pe.eraSeed; act3 = pe.programClassV3ActivationDaa; act4 = pe.programClassV4ActivationDaa;
} 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) });
log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s: template class ${cls} (generator), next epoch class ${nextCls}, era ${era} seed ${String(eraSeed).slice(0, 16)}, activation v3 ${act3} v4 ${act4}`);
if (firstV3 == null && cls === 3) { firstV3 = { epoch, daa, at: +since() }; log(`CLASS SWITCH: the template is class v3 from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
if (firstV4 == null && cls === 4) { firstV4 = { epoch, daa, at: +since() }; log(`CLASS SWITCH: the template is class v4 from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
lastEpoch = epoch;
}
lastDaa = daa ?? lastDaa;
if (Date.now() - lastReport > 15000) {
lastReport = Date.now();
const counts = await Promise.all(nodes.map(async n => { try { const d = await n.rpc.call('getBlockDagInfo'); return `${d.blockCount}/${String(d.sink).slice(0, 8)}`; } catch { return '?'; } }));
log(`t=${since()} s daa ${daa} epoch ${epoch} class ${cls} blocks/sink per node ${counts.join(' ')}`);
samples.push({ t: +since(), daa, epoch, class: cls, nodes: counts });
}
if (firstV4 != null && daa != null && daa >= BOUNDARY + EPOCHS_AFTER * EPOCH) { switchEnd = +since(); break; }
}
await sleep(3000); // let the last blocks relay before the end-of-run reads
// ---- the end-of-run reads -----------------------------------------------------------------------------------------
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return { error: e.message }; } }));
const genesis = dag[0].pruningPointHash;
// every block the first node holds, by DAA score, from genesis
async function allBlocks(n) {
const out = []; let low = genesis; const seen = new Set();
for (let round = 0; round < 500; round++) {
const r = await n.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); out.push({ hash: h, daa: +b.header.daaScore, chain: !!b.verboseData?.isChainBlock, blue: +(b.verboseData?.blueScore ?? 0) }); added++; }
if (!blocks.length || added === 0) break;
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
}
return out;
}
let blocks = [];
try { blocks = await allBlocks(n0); } catch (e) { log(`getBlocks: ${e.message}`); }
const before = blocks.filter(b => b.daa < BOUNDARY), after = blocks.filter(b => b.daa >= BOUNDARY);
const chainBefore = before.filter(b => b.chain).length, chainAfter = after.filter(b => b.chain).length;
// program ids and classes per epoch from the miners' "program and 256 MiB cache ready" lines
const programs = new Map(); // epoch seed -> { class, id, miners: Set }
for (const i of [0, 1, 2]) for (const l of minerLog(i)) {
const m = /epoch seed ([0-9a-f]{64}) day (\d+) \(daa (\d+)\): program and 256 MiB cache ready in ([\d.]+) ms; class (v\d) program id ([0-9a-f]{16})/.exec(l);
if (!m) continue;
const k = m[1]; const e = programs.get(k) || { seed: k.slice(0, 16), epoch: Math.floor(+m[3] / EPOCH), class: m[5], id: m[6], miners: new Set(), ready_ms: [] };
if (e.id !== m[6] || e.class !== m[5]) e.disagree = true;
e.miners.add(i); e.ready_ms.push(+m[4]); programs.set(k, e);
}
// ready_ms: the program generation plus the day's cache and dataset build on one CPU core (the first epoch of a day
// pays the cache; a v4 epoch after a v3 one reuses the v3 day cache, so its number is the program alone)
const programRows = [...programs.values()].sort((a, b) => a.epoch - b.epoch).map(p => ({ epoch: p.epoch, class: p.class, program_id: p.id, seed: p.seed, miners: p.miners.size, disagree: !!p.disagree, ready_ms: p.ready_ms }));
const idsOf = (c) => programRows.filter(p => p.class === c).map(p => p.program_id);
// the CLI's ids for the same seed and era: `igneum-pow show --epoch-hex <seed> --program-class v3|v4 --era-hex <era>`
function cliId(seedHex, eraHex, cls) {
if (!existsSync(IGNEUM_POW)) return null;
const r = spawnSync(IGNEUM_POW, ['show', '--epoch-hex', seedHex, '--program-class', cls, '--era-hex', eraHex], { encoding: 'utf8' });
const m = /program id ([0-9a-f]{16})/.exec(r.stdout || '');
return m ? m[1] : null;
}
const idRows = [];
for (const [k, e] of programs) {
if (e.class !== 'v4') continue;
const era = epochs.get(e.epoch)?.eraSeed;
if (!era) { idRows.push({ epoch: e.epoch, seed: e.seed, era: null, miners_id: e.id, cli_v3: null, cli_v4: null }); continue; }
idRows.push({ epoch: e.epoch, seed: e.seed, era: era.slice(0, 16), miners_id: e.id, miners: e.miners.size, cli_v3: cliId(k, era, 'v3'), cli_v4: cliId(k, era, 'v4') });
}
const idAgainst = (r) => ID_AGAINST === 'v4' ? r.cli_v4 : r.cli_v3;
const v2Ids = idsOf('v2'), v3Ids = idsOf('v3'), v4Ids = idsOf('v4');
const olderIds = [...v2Ids, ...v3Ids];
// rejections: the miners' submit answers and the nodes' PoW lines
const accepted = [0, 1, 2].map(i => minerLog(i).filter(l => /ACCEPTED block/.test(l)).length);
const rejectedMiner = [0, 1, 2].map(i => minerLog(i).filter(l => /rejected nonce=|submit error/.test(l)));
const rejectedNode = nodes.map(n => n.grepLog(/PoW rejected|Rejected block|rejected block/i));
const switchLines = nodes.map(n => n.grepLog(V4_LINE).map(l => tail(l, 'v4'))[0] || null);
const v3SwitchLines = nodes.map(n => n.grepLog(V3_LINE).map(l => tail(l, 'v3'))[0] || null);
const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
const counts = dag.map(d => d.blockCount ?? '?');
const tips = dag.map(d => (d.tipHashes || []).length);
// the Metal miner's protocol lines (gate G4b): PREPARE sent (the miner), prepared / need / error (the worker), the swap
let metal = null;
if (METAL) {
const L = minerLog(0);
const t = (l) => { const m = /^(\d+\.\d+) /.exec(l); return m ? +m[1] * 1000 : null; };
const switchAt = firstV4 ? t0 + firstV4.at * 1000 : null;
const prepares = L.filter(l => /PREPARE sent for epoch seed/.test(l)).map(l => l.replace(/^\S+ /, ''));
const prepared = L.filter(l => /worker: prepared /.test(l)).map(l => l.replace(/^\S+ /, ''));
const preparedV4 = prepared.filter(l => / class v4 /.test(l));
const need = L.filter(l => /worker: need |^\S+ worker error:.*need /.test(l) || /\bneed [0-9a-f]{64}/.test(l));
// a PACK OUT OF DATE before the first class v4 prepare is the v2/v3 rebuild path at work (a seed that flipped inside
// the quarter-lead confirm window, 3 DAA on the 60x profile); the gate judges the v4 path from its first prepare on
const firstV4Prepare = L.findIndex(l => /PREPARE sent for epoch seed .* class v4/.test(l));
const sinceV4 = (l, i) => firstV4Prepare < 0 || i >= firstV4Prepare;
const mismatch = L.filter((l, i) => sinceV4(l, i) && /program class mismatch|era seed mismatch|PACK OUT OF DATE|pack .*: program pack/.test(l));
const mismatchBefore = L.filter((l, i) => !sinceV4(l, i) && /program class mismatch|era seed mismatch|PACK OUT OF DATE/.test(l));
const refused = L.filter(l => /exiting with code 4[24]|refused the program pack|prepare-failed/.test(l));
const swaps = L.filter(l => /swapped with no pause|compiles inline|worker without prepare support/.test(l)).map(l => l.replace(/^\S+ /, ''));
const accepted = L.filter(l => /ACCEPTED block/.test(l));
const acceptedAfter = switchAt ? accepted.filter(l => (t(l) || 0) >= switchAt).length : 0;
const found = L.filter(l => /worker: found |^\S+ found /.test(l)).length;
const status = L.filter(l => /miner 'metal0' \[igneum-pow\]/.test(l)).map(l => l.replace(/^\S+ /, ''));
const lastStatus = status.at(-1) || '';
const mismatched = +(/mismatched=(\d+)/.exec(lastStatus)?.[1] ?? 0);
const rate = /hash=([\d.]+) MH\/s/.exec(lastStatus)?.[1];
metal = { worker: METAL, prepares, prepared, prepared_v4: preparedV4, prepared_v3: prepared.filter(l => / class v3 /.test(l)), need: need.length, mismatch_lines: mismatch, rebuild_lines_before_v4: mismatchBefore, refused, swaps, accepted_total: accepted.length, accepted_after_switch: acceptedAfter, found_lines: found, cpu_recheck_mismatched: mismatched, rate_mh_s: rate, last_status: lastStatus };
}
const checks = {
v4_switch_line_on_every_node: switchLines.every(Boolean),
v4_switch_line_names_the_rounded_epoch: switchLines.every(l => l && FIRST_V4_EPOCH != null && l.includes(`active from epoch ${FIRST_V4_EPOCH} `)),
v3_switch_line_on_every_node: v3SwitchLines.every(Boolean),
template_switched_at_the_first_v4_epoch: firstV4 != null && firstV4.epoch === FIRST_V4_EPOCH,
template_switched_at_the_first_v3_epoch: FIRST_V3_EPOCH == null ? firstV3 == null : (firstV3 != null && firstV3.epoch === FIRST_V3_EPOCH),
blocks_before_the_boundary: before.length > 0,
blocks_after_the_boundary: after.length > 0,
v3_and_v4_programs_seen: v3Ids.length > 0 && v4Ids.length > 0,
v2_programs_seen_when_v3_starts_later: FIRST_V3_EPOCH == null || FIRST_V3_EPOCH === 0 || v2Ids.length > 0,
program_ids_differ_across_the_switch: v4Ids.length > 0 && !olderIds.some(id => v4Ids.includes(id)),
miners_agree_on_every_program: programRows.every(p => !p.disagree),
v4_ids_equal_the_cli_v4_id_of_the_seed_and_era: 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 => idAgainst(r) != null && idAgainst(r) !== r.miners_id),
zero_rejected_by_miners: rejectedMiner.every(r => r.length === 0),
zero_rejected_by_nodes: rejectedNode.every(r => r.length === 0),
sinks_agree: new Set(sinks).size === 1,
block_counts_agree: new Set(counts.map(String)).size === 1,
...(METAL ? {
metal_prepare_sent_for_v4: metal.prepares.some(l => / class v4 /.test(l)),
metal_worker_prepared_v4_pack: metal.prepared_v4.length > 0,
metal_no_need_or_mismatch: metal.need === 0 && metal.mismatch_lines.length === 0 && metal.refused.length === 0,
metal_accepted_blocks_after_switch: metal.accepted_after_switch > 0,
metal_cpu_recheck_clean: metal.cpu_recheck_mismatched === 0,
metal_swapped_without_pause: metal.swaps.some(l => /swapped with no pause/.test(l)),
} : {}),
};
const pass = Object.values(checks).every(Boolean);
const summary = {
pass, checks, activation: ACTIVATION ?? 'never', v3_activation: V3_ACTIVATION ?? 'never', epoch_blocks: EPOCH, first_v3_epoch: FIRST_V3_EPOCH, first_v4_epoch: FIRST_V4_EPOCH, boundary_daa: Number.isFinite(BOUNDARY) ? BOUNDARY : 'never', secs: SECS, threads: THREADS,
node: IGNEUMD, miner: CPU_MINER, metal_worker: METAL, genesis_bits: `0x${GENESIS_BITS.toString(16)}`,
template_switch_v3: firstV3, template_switch: firstV4, run_ended_at_s: switchEnd, final_daa: lastDaa,
blocks: { total: blocks.length, before_boundary: before.length, after_boundary: after.length, chain_before: chainBefore, chain_after: chainAfter },
programs: programRows, program_id_rows: idRows, id_check_against: ID_AGAINST, igneum_pow: IGNEUM_POW, accepted_per_miner: accepted,
rejected_by_miners: rejectedMiner.map(r => r.length), rejected_by_nodes: rejectedNode.map(r => r.length),
rejected_lines: [...rejectedMiner.flat(), ...rejectedNode.flat()].slice(0, 20),
sinks, block_counts: counts, tips_per_node: tips, switch_lines: switchLines, v3_switch_lines: v3SwitchLines, samples, metal,
};
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'}: ${firstV4 ? `v4 from epoch ${firstV4.epoch} at DAA ${firstV4.daa}` : 'NO v4 epoch seen'}; blocks ${before.length} before / ${after.length} after the boundary at DAA ${Number.isFinite(BOUNDARY) ? BOUNDARY : 'never'} (chain ${chainBefore} / ${chainAfter}); programs ${programRows.map(p => `e${p.epoch}:${p.class}:${p.program_id}:${Math.max(...p.ready_ms)}ms`).join(' ')}; rejected miners ${rejectedMiner.map(r => r.length).join('/')} nodes ${rejectedNode.map(r => r.length).join('/')}; sinks ${sinks.join(' ')} (${checks.sinks_agree ? 'agree' : 'DIFFER'}); block counts ${counts.join('/')}; v4 switch lines ${switchLines.filter(Boolean).length}/3, v3 switch lines ${v3SwitchLines.filter(Boolean).length}/3`);
if (metal) {
log(`METAL: ${metal.prepares.length} PREPARE lines (${metal.prepares.filter(l => / class v4 /.test(l)).length} class v4, ${metal.prepares.filter(l => / class v3 /.test(l)).length} class v3), ${metal.prepared.length} prepared (${metal.prepared_v4.length} v4, ${metal.prepared_v3.length} v3), need ${metal.need}, mismatch/refusal lines ${metal.mismatch_lines.length + metal.refused.length}, accepted ${metal.accepted_total} (${metal.accepted_after_switch} after the switch), cpu re-check mismatched ${metal.cpu_recheck_mismatched}, rate ${metal.rate_mh_s} MH/s`);
for (const l of [...metal.prepares, ...metal.prepared, ...metal.swaps]) log(` ${l.slice(0, 260)}`);
for (const l of [...metal.mismatch_lines, ...metal.refused].slice(0, 10)) log(` BAD ${l.slice(0, 260)}`);
}
for (const r of idRows) log(`PROGRAM ID epoch ${r.epoch} seed ${r.seed} era ${r.era}: miners ${r.miners_id} (${r.miners ?? '?'} of 3) cli v4 ${r.cli_v4} cli v3 ${r.cli_v3} ${r.cli_v4 === r.miners_id && r.cli_v3 !== r.miners_id ? 'OK' : 'MISMATCH'}`);
for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`);
log(`summary: ${TMP}/summary.json`);
await stopAll();
process.exit(pass ? 0 : 1);

View file

@ -55,15 +55,16 @@
"proving_v0_activation_daa": 18446744073709551615,
"finality_v3_activation_daa": 18446744073709551615,
"program_class_v3_activation_daa": 18446744073709551615,
"program_class_v4_activation_daa": 18446744073709551615,
"proving_v1_fresh_rule_daa": 18446744073709551615,
"exec_restart_number": 18446744073709551615,
"exec_restart_hash": "",
"exec_restart_trust_daa": 18446744073709551615,
"pow_genesis_dataset_log2": 28,
"proving_v1_activation_daa": 18446744073709551615,
"proving_v1_segment_blocks": 8,
"proving_v1_unproven_daa": 10,
"proving_v1_aggregator_share_bps": 1000,
"fees_v1_activation_daa": 0,
"proving_v1_activation_daa": 18446744073709551615,
"proving_v1_segment_blocks": 8,
"proving_v1_unproven_daa": 10,
"proving_v1_aggregator_share_bps": 1000,
"fees": {"pgas": {"version": 1, "cycles_per_pgas": 1000, "intrinsic_pgas_per_tx": 300, "modexp_base": 10, "modexp_per_byte_numer": 1, "modexp_per_byte_denom": 10}, "block_proving_gas_limit": 120000, "shard_proving_gas_budget": 30000, "min_execution_base_fee_wei": 100000000000, "min_proving_base_fee_wei": 10000000000000, "initial_execution_base_fee_wei": 100000000000, "initial_proving_base_fee_wei": 10000000000000, "base_fee_change_denominator": 8}
}

View file

@ -130,6 +130,38 @@ int main(int argc, char** argv) {
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v3", EPOCH_34, why, sizeof(why)) == 0 && strstr(why, "era seed mismatch") == why, "a job naming another era refuses the v3 pack");
CHECK(pf_pack_class_ok("v2", "", "v3", EPOCH_33, why, sizeof(why)) == 0, "a job naming class v3 refuses a v2 pack");
CHECK(pf_pack_class_ok("v2", "", "v2", EPOCH_33, why, sizeof(why)) == 1, "an era named on a v2 job is ignored");
// Counter ASIC 3.0 (6 October 2026): a generator 4 pack is class v4 with its era seed; a job naming class v3
// refuses it and a job naming class v4 refuses a v3 pack; the era rule applies to v4 as to v3
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 4\n#define IGNEUM_PROGRAM_CLASS \"v4\"\n#define IGNEUM_ERA_SEED_HEX \"%s\"\n#define IGNEUM_SHADOW_INSTRS 256\n#define IGNEUM_SHADOW_REPS 27\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, EPOCH_33, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 1 && strcmp(pk.programClass, "v4") == 0 && strcmp(pk.eraHex, EPOCH_33) == 0, "a generator 4 pack loads as class v4 with its era seed");
// the 6 October trap: the same text stamped generator 3 (a v4 program exported as v3, the v3 control's id) is refused; generator 4 without the block too
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 3\n#define IGNEUM_PROGRAM_CLASS \"v3\"\n#define IGNEUM_ERA_SEED_HEX \"%s\"\n#define IGNEUM_SHADOW_INSTRS 256\n#define IGNEUM_SHADOW_REPS 27\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, EPOCH_33, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "with a shadow block") != NULL, "a generator 3 pack with a shadow block (a v4 program stamped v3) is refused");
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 4\n#define IGNEUM_PROGRAM_CLASS \"v4\"\n#define IGNEUM_ERA_SEED_HEX \"%s\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, EPOCH_33, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "without IGNEUM_SHADOW_INSTRS") != NULL, "a generator 4 pack without the shadow block is refused");
// back to the good v4 pack for the class-token checks below
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 4\n#define IGNEUM_PROGRAM_CLASS \"v4\"\n#define IGNEUM_ERA_SEED_HEX \"%s\"\n#define IGNEUM_SHADOW_INSTRS 256\n#define IGNEUM_SHADOW_REPS 27\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, EPOCH_33, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 1, "the v4 pack with its block loads again");
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v4", EPOCH_33, why, sizeof(why)) == 1, "the v4 pack matches a job naming class v4 and its era");
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v3", EPOCH_33, why, sizeof(why)) == 0 && strstr(why, "program class mismatch") == why, "a job naming class v3 refuses the v4 pack");
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v4", EPOCH_34, why, sizeof(why)) == 0 && strstr(why, "era seed mismatch") == why, "a job naming another era refuses the v4 pack");
CHECK(pf_pack_class_ok("v3", EPOCH_33, "v4", EPOCH_33, why, sizeof(why)) == 0, "a job naming class v4 refuses a v3 pack");
{
char c[8] = {0}, e[65] = {0};
CHECK(pf_class_token("class=v4", c, sizeof(c), e, sizeof(e)) == 1 && strcmp(c, "v4") == 0, "the class=v4 token is read");
}
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 4\n#define IGNEUM_PROGRAM_CLASS \"v3\"\n#define IGNEUM_SHADOW_INSTRS 256\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "does not match IGNEUM_GENERATOR 4") != NULL, "a v3 class line on a generator 4 pack is refused");
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 3\n#define IGNEUM_PROGRAM_CLASS \"v2\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;

View file

@ -29,8 +29,8 @@ typedef struct {
// read-width experiment (5 October 2026): the load class (0 when absent), bytes per hash, variant 5's scratch
uint32_t loadsPerHash, bytesPerHash, scratchOps, persistent, scratchWordsPerLane;
char loadClass[64];
char programClass[8]; /* IGNEUM_PROGRAM_CLASS: "v2" or "v3" (Counter ASIC 2.0); absent = the generator's class */
char eraHex[65]; /* IGNEUM_ERA_SEED_HEX of a class v3 chain pack; empty otherwise */
char programClass[8]; /* IGNEUM_PROGRAM_CLASS: "v2", "v3" (Counter ASIC 2.0) or "v4" (Counter ASIC 3.0); absent = the generator's class */
char eraHex[65]; /* IGNEUM_ERA_SEED_HEX of a class v3 or v4 chain pack; empty otherwise */
// Counter ASIC 2.0 (5 October 2026): the mixer multiplier of the item derivation (IGNEUM_MIXER_MULT, 1 when absent:
// version 2; 4 under class v3). The emitted memhard.h / kernel.cl carry it in their text; this is for the log lines.
uint32_t mixerMult;
@ -284,12 +284,26 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
if (!pf_define_u32(prog, "IGNEUM_CACHE_SEGMENTS", &pk->cacheSegments)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_CACHE_SEGMENTS"); }
if (!pf_define_u32(prog, "IGNEUM_GENERATOR", &pk->generator)) pk->generator = 1;
/* Spec 01 section 1.4.5: a pack whose generator version is not one this worker runs is refused. Generator 2 is
* program class v2 (the lottery hash of 4 October 2026), generator 3 is class v3 (Counter ASIC 2.0). */
if (pk->generator != 2 && pk->generator != 3) {
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2 or 3)", (unsigned)pk->generator);
* program class v2 (the lottery hash of 4 October 2026), generator 3 is class v3 (Counter ASIC 2.0), generator 4
* is class v4 (Counter ASIC 3.0, 6 October 2026: class v3 plus the latency-shadow block, emitted in the pack's own
* kernel text, so this loader needs nothing new beyond the number and the class token). */
if (pk->generator != 2 && pk->generator != 3 && pk->generator != 4) {
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2, 3 or 4)", (unsigned)pk->generator);
free(prog); return pf_fail(err, cap, m);
}
strcpy(pk->programClass, pk->generator == 3 ? "v3" : "v2");
strcpy(pk->programClass, pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2");
{
/* Counter ASIC 3.0 (6 October 2026): the shadow block marks class v4. A generator 3 pack with IGNEUM_SHADOW_INSTRS
* is a v4 program stamped as v3 (the old export path; it carried the v3 control's program id) and is refused;
* a generator 4 pack without the block is no v4 pack. */
uint32_t shadow = 0;
if (!pf_define_u32(prog, "IGNEUM_SHADOW_INSTRS", &shadow)) shadow = 0;
if (pk->generator == 4 && shadow == 0) { free(prog); return pf_fail(err, cap, "program pack generator 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack"); }
if (pk->generator == 3 && shadow != 0) { /* a generator 2 pack with a shadow is the measurement ladder (a class-bearing id) and loads */
char m[220]; snprintf(m, sizeof(m), "program pack generator %u with a shadow block (IGNEUM_SHADOW_INSTRS %u): a class v4 program is generator 4 (export the pack as class v4)", (unsigned)pk->generator, (unsigned)shadow);
free(prog); return pf_fail(err, cap, m);
}
}
{
char named[8] = {0};
if (pf_define_str(prog, "IGNEUM_PROGRAM_CLASS", named, sizeof(named)) && strcmp(named, pk->programClass) != 0) {
@ -429,16 +443,16 @@ static int pf_selftest(const PfPack* pk, const uint32_t* cacheHead, const uint32
}
/* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=<v2|v3>` and `era=<hex>` tokens (sent
* only when the chain is on class v3, so every v2 line is the line of before). A pack matches the line when its class
* is the named class and, when an era is named, its era seed is that era. Empty wanted strings accept any pack.
* Returns 1 on a match, else 0 with the reason in `why`. */
/* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=<v2|v3|v4>` and `era=<hex>` tokens (sent
* only when the chain is on class v3 or v4, so every v2 line is the line of before). A pack matches the line when its
* class is the named class and, when an era is named, its era seed is that era (every class after v2 carries one).
* Empty wanted strings accept any pack. Returns 1 on a match, else 0 with the reason in `why`. */
static int pf_pack_class_ok(const char* packClass, const char* packEra, const char* wantClass, const char* wantEra, char* why, size_t cap) {
if (wantClass && wantClass[0] && strcmp(wantClass, packClass) != 0) {
snprintf(why, cap, "program class mismatch: this pack is class %s, the job names class %s (export the pack again)", packClass, wantClass);
return 0;
}
if (wantEra && wantEra[0] && strcmp(packClass, "v3") == 0) {
if (wantEra && wantEra[0] && strcmp(packClass, "v2") != 0) {
size_t i; int eq = strlen(packEra) == strlen(wantEra);
for (i = 0; eq && packEra[i]; ++i) if (tolower((unsigned char)packEra[i]) != tolower((unsigned char)wantEra[i])) eq = 0;
if (!eq) { snprintf(why, cap, "era seed mismatch: this pack was drawn under era %.16s, the job names era %.16s (export the pack again)", packEra[0] ? packEra : "(none)", wantEra); return 0; }

View file

@ -13,9 +13,9 @@
#define IGNEUM_SEED_STRING "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_SEED_BYTES_HEX "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"
#define IGNEUM_GENERATOR 3
#define IGNEUM_GENERATOR 4
#define IGNEUM_PROGRAM_ATTEMPT 0
#define IGNEUM_PROGRAM_ID 0x73bcbfe8ccf988f1ull
#define IGNEUM_PROGRAM_ID 0xc120d7963abdcd96ull
#define IGNEUM_DAY_STRING "bytes:69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY_BYTES_HEX "69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY0 0xceed56d7u
@ -28,9 +28,10 @@
#define IGNEUM_LOADS_PER_HASH 128
#define IGNEUM_WIDE_LOADS_PER_HASH 0
#define IGNEUM_OP_MIX "load=16 add=15 shfl=6 mad=4 or=4 rotl=4 rotr=4 xor=4 mulhi=3 mul=2 sub=2"
// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that
// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v3"
// Program class v4 (Counter ASIC 3.0, docs/plans/counter-asic-3-node.md): generator version 4, class v3 plus the
// latency-shadow block (IGNEUM_SHADOW_INSTRS x IGNEUM_SHADOW_REPS per iteration); a worker that runs another class
// refuses this pack, and a job line names the class it wants (class=v4 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v4"
#define IGNEUM_ERA_SEED_HEX "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"
// Class v3 construction (Counter ASIC 2.0, 5 October 2026, docs/plans/mixer-x4.md): version 2 loads; the dataset item
// derivation applies the mixer IGNEUM_MIXER_MULT times per round (memhard.h), and the cache follows the growth rule.

View file

@ -1,8 +1,8 @@
{
"format": "igneum-program-pack-3",
"generator": 3,
"generator": 4,
"attempt": 0,
"program_id": "0x73bcbfe8ccf988f1",
"program_id": "0xc120d7963abdcd96",
"program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32",
"dataset_mode": "memory-hard",
"seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000",
@ -15,7 +15,7 @@
"iterations": 8,
"instruction_count": 64,
"loads_per_hash": 128,
"program_class": "v3",
"program_class": "v4",
"era_seed_bytes": "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07",
"load_class": "mx8-erad810f22d+sh256x27",
"mixer_mult": 8,

View file

@ -1,5 +1,5 @@
// Generated by igneum-pow export (generator v2) for seed "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000". Do not edit by hand.
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset
#pragma once
#ifdef __cplusplus
#include <cstdint>

View file

@ -5,7 +5,7 @@
"dataset_log2_words": 28,
"mask": "0x0fffffff",
"lanes": 32,
"source": "igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset",
"source": "igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset",
"warps": [
{"base_nonce": 0, "expected": [
"0xcefc114b62c2013b", "0x82df54bee88f1fa9", "0x94ed24d32cecf436", "0x8a9bd67e3fed0c5b", "0x7c1a21514ebe42b7", "0x0edd5b084c5815ed", "0x077927721192da87", "0x28e26e3eb72cd8c4",

View file

@ -13,9 +13,9 @@
#define IGNEUM_SEED_STRING "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_SEED_BYTES_HEX "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"
#define IGNEUM_GENERATOR 3
#define IGNEUM_GENERATOR 4
#define IGNEUM_PROGRAM_ATTEMPT 0
#define IGNEUM_PROGRAM_ID 0x73bcbfe8ccf988f1ull
#define IGNEUM_PROGRAM_ID 0xc120d7963abdcd96ull
#define IGNEUM_DAY_STRING "bytes:69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY_BYTES_HEX "69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY0 0xceed56d7u
@ -28,9 +28,10 @@
#define IGNEUM_LOADS_PER_HASH 128
#define IGNEUM_WIDE_LOADS_PER_HASH 0
#define IGNEUM_OP_MIX "load=16 add=15 shfl=6 mad=4 or=4 rotl=4 rotr=4 xor=4 mulhi=3 mul=2 sub=2"
// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that
// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v3"
// Program class v4 (Counter ASIC 3.0, docs/plans/counter-asic-3-node.md): generator version 4, class v3 plus the
// latency-shadow block (IGNEUM_SHADOW_INSTRS x IGNEUM_SHADOW_REPS per iteration); a worker that runs another class
// refuses this pack, and a job line names the class it wants (class=v4 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v4"
#define IGNEUM_ERA_SEED_HEX "8bffdd3366b9c3ffe89c1231e91538d531cfa717307df5c48ccba43096e17212"
// Class v3 construction (Counter ASIC 2.0, 5 October 2026, docs/plans/mixer-x4.md): version 2 loads; the dataset item
// derivation applies the mixer IGNEUM_MIXER_MULT times per round (memhard.h), and the cache follows the growth rule.

View file

@ -1,8 +1,8 @@
{
"format": "igneum-program-pack-3",
"generator": 3,
"generator": 4,
"attempt": 0,
"program_id": "0x73bcbfe8ccf988f1",
"program_id": "0xc120d7963abdcd96",
"program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32",
"dataset_mode": "memory-hard",
"seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000",
@ -15,7 +15,7 @@
"iterations": 8,
"instruction_count": 64,
"loads_per_hash": 128,
"program_class": "v3",
"program_class": "v4",
"era_seed_bytes": "8bffdd3366b9c3ffe89c1231e91538d531cfa717307df5c48ccba43096e17212",
"load_class": "mx8-erab2ed8a89+sh256x27",
"mixer_mult": 8,

View file

@ -1,5 +1,5 @@
// Generated by igneum-pow export (generator v2) for seed "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000". Do not edit by hand.
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset
#pragma once
#ifdef __cplusplus
#include <cstdint>

View file

@ -5,7 +5,7 @@
"dataset_log2_words": 28,
"mask": "0x0fffffff",
"lanes": 32,
"source": "igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset",
"source": "igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset",
"warps": [
{"base_nonce": 0, "expected": [
"0xc6ac65d8c667c94b", "0x423ba68373b366fb", "0x21eb7c55ad8be449", "0x241e717cf2ab882a", "0xa615906f3f9be2ce", "0xce309393aa36b7ef", "0x6243ddb51a337304", "0xe0495ce514ee4267",

View file

@ -13,9 +13,9 @@
#define IGNEUM_SEED_STRING "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_SEED_BYTES_HEX "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"
#define IGNEUM_GENERATOR 3
#define IGNEUM_GENERATOR 4
#define IGNEUM_PROGRAM_ATTEMPT 0
#define IGNEUM_PROGRAM_ID 0x73bcbfe8ccf988f1ull
#define IGNEUM_PROGRAM_ID 0xc120d7963abdcd96ull
#define IGNEUM_DAY_STRING "bytes:69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY_BYTES_HEX "69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY0 0xceed56d7u
@ -28,9 +28,10 @@
#define IGNEUM_LOADS_PER_HASH 128
#define IGNEUM_WIDE_LOADS_PER_HASH 0
#define IGNEUM_OP_MIX "load=16 add=15 shfl=6 mad=4 or=4 rotl=4 rotr=4 xor=4 mulhi=3 mul=2 sub=2"
// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that
// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v3"
// Program class v4 (Counter ASIC 3.0, docs/plans/counter-asic-3-node.md): generator version 4, class v3 plus the
// latency-shadow block (IGNEUM_SHADOW_INSTRS x IGNEUM_SHADOW_REPS per iteration); a worker that runs another class
// refuses this pack, and a job line names the class it wants (class=v4 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v4"
#define IGNEUM_ERA_SEED_HEX "ff87ad96a1b53f367a95d5ca123bab64211bd9aa57fc7ad3c5e2d496c20138d4"
// Class v3 construction (Counter ASIC 2.0, 5 October 2026, docs/plans/mixer-x4.md): version 2 loads; the dataset item
// derivation applies the mixer IGNEUM_MIXER_MULT times per round (memhard.h), and the cache follows the growth rule.

View file

@ -1,8 +1,8 @@
{
"format": "igneum-program-pack-3",
"generator": 3,
"generator": 4,
"attempt": 0,
"program_id": "0x73bcbfe8ccf988f1",
"program_id": "0xc120d7963abdcd96",
"program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32",
"dataset_mode": "memory-hard",
"seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000",
@ -15,7 +15,7 @@
"iterations": 8,
"instruction_count": 64,
"loads_per_hash": 128,
"program_class": "v3",
"program_class": "v4",
"era_seed_bytes": "ff87ad96a1b53f367a95d5ca123bab64211bd9aa57fc7ad3c5e2d496c20138d4",
"load_class": "mx8-era676a17fc+sh256x27",
"mixer_mult": 8,

View file

@ -1,5 +1,5 @@
// Generated by igneum-pow export (generator v2) for seed "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000". Do not edit by hand.
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset
#pragma once
#ifdef __cplusplus
#include <cstdint>

View file

@ -5,7 +5,7 @@
"dataset_log2_words": 28,
"mask": "0x0fffffff",
"lanes": 32,
"source": "igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset",
"source": "igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset",
"warps": [
{"base_nonce": 0, "expected": [
"0xbd25be585e9f483a", "0x42415dc1008bc167", "0x808b481a2ba0ee31", "0x9482f0d11a102aac", "0xc8782e4fbd38f71d", "0xbef02f6193c78dfe", "0xf790061d0baf0ee2", "0x8df707bfebbb5266",

View file

@ -13,9 +13,9 @@
#define IGNEUM_SEED_STRING "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_SEED_BYTES_HEX "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"
#define IGNEUM_GENERATOR 3
#define IGNEUM_GENERATOR 4
#define IGNEUM_PROGRAM_ATTEMPT 0
#define IGNEUM_PROGRAM_ID 0x73bcbfe8ccf988f1ull
#define IGNEUM_PROGRAM_ID 0xc120d7963abdcd96ull
#define IGNEUM_DAY_STRING "bytes:69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY_BYTES_HEX "69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY0 0xceed56d7u
@ -28,9 +28,10 @@
#define IGNEUM_LOADS_PER_HASH 128
#define IGNEUM_WIDE_LOADS_PER_HASH 0
#define IGNEUM_OP_MIX "load=16 add=15 shfl=6 mad=4 or=4 rotl=4 rotr=4 xor=4 mulhi=3 mul=2 sub=2"
// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that
// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v3"
// Program class v4 (Counter ASIC 3.0, docs/plans/counter-asic-3-node.md): generator version 4, class v3 plus the
// latency-shadow block (IGNEUM_SHADOW_INSTRS x IGNEUM_SHADOW_REPS per iteration); a worker that runs another class
// refuses this pack, and a job line names the class it wants (class=v4 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v4"
#define IGNEUM_ERA_SEED_HEX "df57136f2ad5f410e6145023090eea148c1342ccef5a5f541afa590be7e44745"
// Class v3 construction (Counter ASIC 2.0, 5 October 2026, docs/plans/mixer-x4.md): version 2 loads; the dataset item
// derivation applies the mixer IGNEUM_MIXER_MULT times per round (memhard.h), and the cache follows the growth rule.

View file

@ -1,8 +1,8 @@
{
"format": "igneum-program-pack-3",
"generator": 3,
"generator": 4,
"attempt": 0,
"program_id": "0x73bcbfe8ccf988f1",
"program_id": "0xc120d7963abdcd96",
"program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32",
"dataset_mode": "memory-hard",
"seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000",
@ -15,7 +15,7 @@
"iterations": 8,
"instruction_count": 64,
"loads_per_hash": 128,
"program_class": "v3",
"program_class": "v4",
"era_seed_bytes": "df57136f2ad5f410e6145023090eea148c1342ccef5a5f541afa590be7e44745",
"load_class": "mx8-era843155d7+sh256x27",
"mixer_mult": 8,

View file

@ -1,5 +1,5 @@
// Generated by igneum-pow export (generator v2) for seed "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000". Do not edit by hand.
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset
#pragma once
#ifdef __cplusplus
#include <cstdint>

View file

@ -5,7 +5,7 @@
"dataset_log2_words": 28,
"mask": "0x0fffffff",
"lanes": 32,
"source": "igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset",
"source": "igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset",
"warps": [
{"base_nonce": 0, "expected": [
"0xd15f7638ecab0638", "0xf71dc04ca33f9049", "0xdda27e825b83045f", "0x0babd54b6eb42cb6", "0x22b4cafe1d3a82d7", "0x1ded091e583999c8", "0x9445769b44b82cbd", "0xbd850d3ec7937586",

View file

@ -13,9 +13,9 @@
#define IGNEUM_SEED_STRING "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_SEED_BYTES_HEX "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"
#define IGNEUM_GENERATOR 3
#define IGNEUM_GENERATOR 4
#define IGNEUM_PROGRAM_ATTEMPT 0
#define IGNEUM_PROGRAM_ID 0x73bcbfe8ccf988f1ull
#define IGNEUM_PROGRAM_ID 0xc120d7963abdcd96ull
#define IGNEUM_DAY_STRING "bytes:69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY_BYTES_HEX "69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY0 0xceed56d7u
@ -28,9 +28,10 @@
#define IGNEUM_LOADS_PER_HASH 128
#define IGNEUM_WIDE_LOADS_PER_HASH 0
#define IGNEUM_OP_MIX "load=16 add=15 shfl=6 mad=4 or=4 rotl=4 rotr=4 xor=4 mulhi=3 mul=2 sub=2"
// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that
// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v3"
// Program class v4 (Counter ASIC 3.0, docs/plans/counter-asic-3-node.md): generator version 4, class v3 plus the
// latency-shadow block (IGNEUM_SHADOW_INSTRS x IGNEUM_SHADOW_REPS per iteration); a worker that runs another class
// refuses this pack, and a job line names the class it wants (class=v4 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v4"
#define IGNEUM_ERA_SEED_HEX "e593fc1d48475c88e456632f6aa0a752a5fdbca3c33022afe37680e44b7dfd11"
// Class v3 construction (Counter ASIC 2.0, 5 October 2026, docs/plans/mixer-x4.md): version 2 loads; the dataset item
// derivation applies the mixer IGNEUM_MIXER_MULT times per round (memhard.h), and the cache follows the growth rule.

View file

@ -1,8 +1,8 @@
{
"format": "igneum-program-pack-3",
"generator": 3,
"generator": 4,
"attempt": 0,
"program_id": "0x73bcbfe8ccf988f1",
"program_id": "0xc120d7963abdcd96",
"program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32",
"dataset_mode": "memory-hard",
"seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000",
@ -15,7 +15,7 @@
"iterations": 8,
"instruction_count": 64,
"loads_per_hash": 128,
"program_class": "v3",
"program_class": "v4",
"era_seed_bytes": "e593fc1d48475c88e456632f6aa0a752a5fdbca3c33022afe37680e44b7dfd11",
"load_class": "mx8-erad6367bfe+sh256x27",
"mixer_mult": 8,

View file

@ -1,5 +1,5 @@
// Generated by igneum-pow export (generator v2) for seed "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000". Do not edit by hand.
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset
#pragma once
#ifdef __cplusplus
#include <cstdint>

View file

@ -5,7 +5,7 @@
"dataset_log2_words": 28,
"mask": "0x0fffffff",
"lanes": 32,
"source": "igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset",
"source": "igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset",
"warps": [
{"base_nonce": 0, "expected": [
"0x40b750d1c8a8070f", "0x0eabee774c28ab59", "0xcc13c43e53c39432", "0x6a9c15036cd8ea3e", "0x33632873969d8ede", "0x598d17a33791fdc9", "0x2eff1230b4254f1e", "0xe94dd51fbd984bce",

View file

@ -13,9 +13,9 @@
#define IGNEUM_SEED_STRING "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_SEED_BYTES_HEX "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"
#define IGNEUM_GENERATOR 3
#define IGNEUM_GENERATOR 4
#define IGNEUM_PROGRAM_ATTEMPT 0
#define IGNEUM_PROGRAM_ID 0x73bcbfe8ccf988f1ull
#define IGNEUM_PROGRAM_ID 0xc120d7963abdcd96ull
#define IGNEUM_DAY_STRING "bytes:69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY_BYTES_HEX "69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY0 0xceed56d7u
@ -28,9 +28,10 @@
#define IGNEUM_LOADS_PER_HASH 128
#define IGNEUM_WIDE_LOADS_PER_HASH 0
#define IGNEUM_OP_MIX "load=16 add=15 shfl=6 mad=4 or=4 rotl=4 rotr=4 xor=4 mulhi=3 mul=2 sub=2"
// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that
// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v3"
// Program class v4 (Counter ASIC 3.0, docs/plans/counter-asic-3-node.md): generator version 4, class v3 plus the
// latency-shadow block (IGNEUM_SHADOW_INSTRS x IGNEUM_SHADOW_REPS per iteration); a worker that runs another class
// refuses this pack, and a job line names the class it wants (class=v4 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v4"
#define IGNEUM_ERA_SEED_HEX "5e0587f455a86e91e4990f5c481a34cca0044d3f3ae519adcae58ddc82885d31"
// Class v3 construction (Counter ASIC 2.0, 5 October 2026, docs/plans/mixer-x4.md): version 2 loads; the dataset item
// derivation applies the mixer IGNEUM_MIXER_MULT times per round (memhard.h), and the cache follows the growth rule.

View file

@ -1,8 +1,8 @@
{
"format": "igneum-program-pack-3",
"generator": 3,
"generator": 4,
"attempt": 0,
"program_id": "0x73bcbfe8ccf988f1",
"program_id": "0xc120d7963abdcd96",
"program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32",
"dataset_mode": "memory-hard",
"seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000",
@ -15,7 +15,7 @@
"iterations": 8,
"instruction_count": 64,
"loads_per_hash": 128,
"program_class": "v3",
"program_class": "v4",
"era_seed_bytes": "5e0587f455a86e91e4990f5c481a34cca0044d3f3ae519adcae58ddc82885d31",
"load_class": "mx8-era4488f3ed+sh256x27",
"mixer_mult": 8,

View file

@ -1,5 +1,5 @@
// Generated by igneum-pow export (generator v2) for seed "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000". Do not edit by hand.
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset
#pragma once
#ifdef __cplusplus
#include <cstdint>

View file

@ -5,7 +5,7 @@
"dataset_log2_words": 28,
"mask": "0x0fffffff",
"lanes": 32,
"source": "igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset",
"source": "igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset",
"warps": [
{"base_nonce": 0, "expected": [
"0x32d75a818461b0fe", "0xfb58b5a9a0749f1a", "0x7f1ef1c14a39acb7", "0x241bbf0d64ef82da", "0xa91a888a98e1a327", "0x85236aa911aee1cc", "0x2b621fc52bd4de06", "0xccb36c7b29b9f7ee",

View file

@ -13,9 +13,9 @@
#define IGNEUM_SEED_STRING "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_SEED_BYTES_HEX "edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07"
#define IGNEUM_GENERATOR 3
#define IGNEUM_GENERATOR 4
#define IGNEUM_PROGRAM_ATTEMPT 0
#define IGNEUM_PROGRAM_ID 0x73bcbfe8ccf988f1ull
#define IGNEUM_PROGRAM_ID 0xc120d7963abdcd96ull
#define IGNEUM_DAY_STRING "bytes:69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY_BYTES_HEX "69676e65756d2d6461792ffa50000000000000"
#define IGNEUM_DAY0 0xceed56d7u
@ -28,9 +28,10 @@
#define IGNEUM_LOADS_PER_HASH 128
#define IGNEUM_WIDE_LOADS_PER_HASH 0
#define IGNEUM_OP_MIX "load=16 add=15 shfl=6 mad=4 or=4 rotl=4 rotr=4 xor=4 mulhi=3 mul=2 sub=2"
// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that
// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v3"
// Program class v4 (Counter ASIC 3.0, docs/plans/counter-asic-3-node.md): generator version 4, class v3 plus the
// latency-shadow block (IGNEUM_SHADOW_INSTRS x IGNEUM_SHADOW_REPS per iteration); a worker that runs another class
// refuses this pack, and a job line names the class it wants (class=v4 era=<hex>).
#define IGNEUM_PROGRAM_CLASS "v4"
#define IGNEUM_ERA_SEED_HEX "7f450623297a954f493ca08abcdfe7142bb753900abcee38256266007b4b48b7"
// Class v3 construction (Counter ASIC 2.0, 5 October 2026, docs/plans/mixer-x4.md): version 2 loads; the dataset item
// derivation applies the mixer IGNEUM_MIXER_MULT times per round (memhard.h), and the cache follows the growth rule.

View file

@ -1,8 +1,8 @@
{
"format": "igneum-program-pack-3",
"generator": 3,
"generator": 4,
"attempt": 0,
"program_id": "0x73bcbfe8ccf988f1",
"program_id": "0xc120d7963abdcd96",
"program_id_derivation": "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32",
"dataset_mode": "memory-hard",
"seed": "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000",
@ -15,7 +15,7 @@
"iterations": 8,
"instruction_count": 64,
"loads_per_hash": 128,
"program_class": "v3",
"program_class": "v4",
"era_seed_bytes": "7f450623297a954f493ca08abcdfe7142bb753900abcee38256266007b4b48b7",
"load_class": "mx8-eraf897c84e+sh256x27",
"mixer_mult": 8,

View file

@ -1,5 +1,5 @@
// Generated by igneum-pow export (generator v2) for seed "igneum-epoch/edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07/day/69676e65756d2d6461792ffa50000000000000". Do not edit by hand.
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset
// Expected outputs: igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset
#pragma once
#ifdef __cplusplus
#include <cstdint>

View file

@ -5,7 +5,7 @@
"dataset_log2_words": 28,
"mask": "0x0fffffff",
"lanes": 32,
"source": "igneum-pow (Rust) CPU interpreter, generator v3, memory-hard dataset",
"source": "igneum-pow (Rust) CPU interpreter, generator v4, memory-hard dataset",
"warps": [
{"base_nonce": 0, "expected": [
"0xbf945be0cc8d3f41", "0x34be8a9b058957ef", "0xa790f250cb3f84c2", "0xf348a3adcb120fee", "0x84ffe7a18320fce6", "0xc3ca0cd342a7b5a0", "0xc661be2913cc0a16", "0x42a2613074355476",

View file

@ -2784,7 +2784,7 @@ final class ServeProgram {
/// The Swift-generated program (class v2); nil for a program compiled from a pack (class v3, Counter ASIC 2.0),
/// which has no Swift instruction list and so never races variants
let program: Program?
/// Counter ASIC 2.0: the class ("v2" or "v3") and, for a pack program, its era seed hex and loads per hash
/// Counter ASIC 2.0: the class ("v2", "v3" or, Counter ASIC 3.0, "v4") and, for a pack program, its era seed hex and loads per hash
let programClass: String
let eraHex: String
let loadsPerHash: Int
@ -2804,7 +2804,7 @@ final class ServeProgram {
/// Whether this program is the one a line naming `cls` (and `era`) wants; empty names accept any (class v2 lines).
func matches(cls: String, era: String) -> Bool {
if cls != "" && cls != programClass { return false }
if era != "" && programClass == "v3" && era.lowercased() != eraHex.lowercased() { return false }
if era != "" && isPackClass(programClass) && era.lowercased() != eraHex.lowercased() { return false }
return true
}
var compiled: CompiledHash { lock.lock(); defer { lock.unlock() }; return slot }
@ -2984,11 +2984,18 @@ final class ServeDataset {
}
func matches(cls: String, era: String) -> Bool {
if cls != "" && cls != programClass { return false }
if era != "" && programClass == "v3" && era.lowercased() != eraHex.lowercased() { return false }
if era != "" && isPackClass(programClass) && era.lowercased() != eraHex.lowercased() { return false }
return true
}
}
/// Counter ASIC 2.0 and 3.0: the classes this worker runs from a prepared pack only (never from the Swift version 2
/// generator): class v3 (generator 3) and class v4 (generator 4, class v3 plus the latency-shadow block, which is in
/// the pack's own program_bound.metal). Each carries an era seed.
func isPackClass(_ cls: String) -> Bool { cls == "v3" || cls == "v4" }
/// The class name of a pack's IGNEUM_GENERATOR (packfile.h's rule).
func packClassOf(generator: UInt32) -> String { generator == 4 ? "v4" : generator == 3 ? "v3" : "v2" }
// The resident programs and datasets, shared by the job loop (main thread) and the prepare queue (background).
final class ServeStore {
private let lock = NSLock()
@ -3053,7 +3060,8 @@ func serveProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [UInt8
// Counter ASIC 2.0 (5 October 2026): a class v3 program comes from the pack igneum-miner export-pack / --prepare-packs
// wrote (program_bound.metal, the Rust emitter's text; program.h for the identity), never from the Swift generator.
// The pack is checked the way the one-click workers check it (proto-cuda/nvrtc/packfile.h): IGNEUM_GENERATOR 2 or 3,
// Counter ASIC 3.0 (6 October 2026): a class v4 program (generator 4) the same way; its shadow block is in that text.
// The pack is checked the way the one-click workers check it (proto-cuda/nvrtc/packfile.h): IGNEUM_GENERATOR 2, 3 or 4,
// the class line against the generator, the seed bytes against the line's epoch seed, IGNEUM_SEEDW_INIT against the
// words of IGNEUM_PROGRAM_ATTEMPT of that seed, and the class and era against what the line names.
func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [UInt8], dir: String, wantClass: String, wantEra: String) throws -> (ServeProgram, Double) {
@ -3077,12 +3085,17 @@ func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [U
}
func refuse(_ why: String) -> NSError { NSError(domain: "pack", code: 2, userInfo: [NSLocalizedDescriptionKey: "pack \(dir): \(why)"]) }
let generator = defineU32("IGNEUM_GENERATOR") ?? 1
guard generator == 2 || generator == 3 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2 or 3)") }
let packClass = generator == 3 ? "v3" : "v2"
guard generator == 2 || generator == 3 || generator == 4 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2, 3 or 4)") }
let packClass = packClassOf(generator: generator)
if let named = defineStr("IGNEUM_PROGRAM_CLASS"), named != packClass { throw refuse("program pack IGNEUM_PROGRAM_CLASS \"\(named)\" does not match IGNEUM_GENERATOR \(generator)") }
// Counter ASIC 3.0 (6 October 2026): the shadow block marks class v4 (packfile.h's rule): a generator 3 pack with
// IGNEUM_SHADOW_INSTRS is a v4 program stamped v3 (the v3 control's program id) and a generator 4 pack without it is no v4 pack
let shadow = defineU32("IGNEUM_SHADOW_INSTRS") ?? 0
if generator == 4 && shadow == 0 { throw refuse("program pack generator 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack") }
if generator == 3 && shadow != 0 { throw refuse("program pack generator \(generator) with a shadow block (IGNEUM_SHADOW_INSTRS \(shadow)): a class v4 program is generator 4 (export the pack as class v4)") }
let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? ""
if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass) (export the pack again)") }
if wantEra != "" && packClass == "v3" && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: this pack was drawn under era \(eraHex.isEmpty ? "(none)" : String(eraHex.prefix(16))), the line names era \(wantEra.prefix(16)) (export the pack again)") }
if wantEra != "" && isPackClass(packClass) && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: this pack was drawn under era \(eraHex.isEmpty ? "(none)" : String(eraHex.prefix(16))), the line names era \(wantEra.prefix(16)) (export the pack again)") }
if let packSeed = defineStr("IGNEUM_SEED_BYTES_HEX"), packSeed.lowercased() != seedHex.lowercased() { throw refuse("the pack is for epoch \(packSeed.prefix(16)), not \(seedHex.prefix(16))") }
let attempt = defineU32("IGNEUM_PROGRAM_ATTEMPT") ?? 0
guard let seedw = defineWords("IGNEUM_SEEDW_INIT") else { throw refuse("program.h has no IGNEUM_SEEDW_INIT with 8 words") }
@ -3128,11 +3141,11 @@ func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: Stri
return String(programH[Range(m.range(at: 1), in: programH)!])
}
let generator = defineU32("IGNEUM_GENERATOR") ?? 1
let packClass = generator == 3 ? "v3" : "v2"
let packClass = packClassOf(generator: generator)
let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? ""
if let packDay = defineStr("IGNEUM_DAY_BYTES_HEX"), packDay.lowercased() != dayHex.lowercased() { throw refuse("the pack's day is \(packDay), not \(dayHex)") }
if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass)") }
if wantEra != "" && packClass == "v3" && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: the pack's era is \(eraHex.prefix(16)), the line names \(wantEra.prefix(16))") }
if wantEra != "" && isPackClass(packClass) && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: the pack's era is \(eraHex.prefix(16)), the line names \(wantEra.prefix(16))") }
if (defineU32("IGNEUM_HOT_MB") ?? 0) > 0 { throw refuse("a hot-table pack (IGNEUM_HOT_MB) is not served by this worker") }
guard (defineU32("IGNEUM_DATASET_MODE") ?? 0) == 1 else { throw refuse("not a memory-hard pack (IGNEUM_DATASET_MODE 1)") }
let datasetLog2 = Int(defineU32("IGNEUM_DATASET_LOG2") ?? 28)
@ -3182,18 +3195,19 @@ func runServe(_ opts: Options) -> Never {
if f.isEmpty { continue }
if f[0] == "quit" { break }
// Counter ASIC 2.0 (5 October 2026): a job or prepare line of a class v3 epoch ends with `class=v3 era=<hex>`
// (a class v2 line is the line of before). A class v2 program is generated here from the seed (the Swift
// version 2 generator); a class v3 program is compiled from the pack the prepare line names (servePackProgram),
// so a v3 job on seeds with no prepared v3 pack asks for one (`need`) instead of mining the wrong program.
// (a class v2 line is the line of before); Counter ASIC 3.0 (6 October 2026): a class v4 epoch's with
// `class=v4 era=<hex>`. A class v2 program is generated here from the seed (the Swift version 2 generator); a
// class v3 or v4 program is compiled from the pack the prepare line names (servePackProgram), so a v3 or v4
// job on seeds with no prepared pack of that class asks for one (`need`) instead of mining the wrong program.
var wantClass = "", wantEra = ""
while let last = f.last, last.hasPrefix("class=") || last.hasPrefix("era=") {
if last.hasPrefix("class=") { wantClass = String(last.dropFirst(6)) } else { wantEra = String(last.dropFirst(4)) }
f.removeLast()
}
if wantClass != "" && wantClass != "v2" && wantClass != "v3" {
if wantClass != "" && wantClass != "v2" && !isPackClass(wantClass) {
let id = f.count > 1 ? f[1] : "0"
if f[0] == "prepare" { emit("prepare-failed \(id) \(f.count > 2 ? f[2] : "0") program class \(wantClass) is not one this worker runs (v2 or v3)") }
else { emit("error \(id) program class \(wantClass) is not one this worker runs (v2 or v3)") }
if f[0] == "prepare" { emit("prepare-failed \(id) \(f.count > 2 ? f[2] : "0") program class \(wantClass) is not one this worker runs (v2, v3 or v4)") }
else { emit("error \(id) program class \(wantClass) is not one this worker runs (v2, v3 or v4)") }
continue
}
if f[0] == "prepare" {
@ -3206,15 +3220,15 @@ func runServe(_ opts: Options) -> Never {
let have = (store.program(epochHex, cls: wantClass, era: wantEra) != nil, store.dataset(dayHex, cls: wantClass, era: wantEra) != nil)
if have.0 && have.1 { emit("prepared \(epochHex) \(dayHex) 0 program 0 dataset 0 (already resident)"); continue }
let packDir = f.count > 3 ? f[3...].joined(separator: " ") : ""
if wantClass == "v3" && packDir.isEmpty { emit("prepare-failed \(epochHex) \(dayHex) a class v3 program is compiled from a pack: send prepare with a pack directory (igneum-miner --prepare-packs <dir>)"); continue }
if isPackClass(wantClass) && packDir.isEmpty { emit("prepare-failed \(epochHex) \(dayHex) a class \(wantClass) program is compiled from a pack: send prepare with a pack directory (igneum-miner --prepare-packs <dir>)"); continue }
prepareQueue.async {
let t0 = nowNs()
do {
let (sp, progMs) = wantClass == "v3"
let (sp, progMs) = isPackClass(wantClass)
? try servePackProgram(gpu, store, seedHex: epochHex, seed: epochSeed, dir: packDir, wantClass: wantClass, wantEra: wantEra)
: try serveProgram(gpu, store, seedHex: epochHex, seed: epochSeed, datasetLog2: datasetLog2)
// a class v3 day is built from the pack (its construction and layout), a v2 day by the Swift context
let (sd, dsMs) = wantClass == "v3"
// a class v3 or v4 day is built from the pack (its construction and layout), a v2 day by the Swift context
let (sd, dsMs) = isPackClass(wantClass)
? try servePackDataset(gpu, store, dayHex: dayHex, dir: packDir, wantClass: wantClass, wantEra: wantEra)
: serveDataset(gpu, store, dayHex: dayHex, day: daySeed, datasetLog2: datasetLog2)
// The race: the prepared program against its own dataset, exclusive windows between jobs
@ -3226,7 +3240,7 @@ func runServe(_ opts: Options) -> Never {
}
let (np, nd) = store.counts()
emit("prepared \(epochHex) \(dayHex) \(fmt(ms(t0, nowNs()), 1)) program \(fmt(progMs, 1)) dataset \(fmt(dsMs, 1)) race \(fmt(raceMs, 1)) variant \(sp.compiled.variant) class \(sp.programClass) loads/hash \(sp.loadsPerHash) cache-fill \(fmt(sd.cacheFillGPUms, 1)) build \(fmt(sd.buildGPUms, 1)) resident \(np) programs \(nd) datasets")
} catch { emit("prepare-failed \(epochHex) \(dayHex) \(wantClass == "v3" ? "" : "Metal compile failed: ")\(error.localizedDescription)") }
} catch { emit("prepare-failed \(epochHex) \(dayHex) \(isPackClass(wantClass) ? "" : "Metal compile failed: ")\(error.localizedDescription)") }
}
continue
}
@ -3251,12 +3265,12 @@ func runServe(_ opts: Options) -> Never {
if store.program(pair.0, cls: wantClass, era: wantEra) == nil || store.dataset(pair.1, cls: wantClass, era: wantEra) == nil { prepareQueue.sync {} }
let sp: ServeProgram
let sd: ServeDataset
if wantClass == "v3" {
// a v3 job runs on the pack program and the pack-built day of its class and era, both prepared; nothing
// is compiled inline (the Swift generator and context are the version 2 construction)
if isPackClass(wantClass) {
// a v3 or v4 job runs on the pack program and the pack-built day of its class and era, both prepared;
// nothing is compiled inline (the Swift generator and context are the version 2 construction)
guard let resident = store.program(pair.0, cls: wantClass, era: wantEra), let day = store.dataset(pair.1, cls: wantClass, era: wantEra) else {
emit("need \(pair.0) \(pair.1)")
emit("error \(jobId) pack (metal): program class mismatch: no class v3 program and day for epoch \(pair.0.prefix(16)) day \(pair.1) is resident (this worker builds v3 from a pack); send prepare with a pack directory")
emit("error \(jobId) pack (metal): program class mismatch: no class \(wantClass) program and day for epoch \(pair.0.prefix(16)) day \(pair.1) is resident (this worker builds \(wantClass) from a pack); send prepare with a pack directory")
continue
}
sp = resident; sd = day

393
proto-opencl/family-probe.c Normal file
View file

@ -0,0 +1,393 @@
/* family-probe (OpenCL): the step cost of every reserve candidate family of spec 1.13.2 on AMD (and any OpenCL GPU),
* standalone (no pack, no lottery kernel). Counter ASIC 3.0 item 6 (docs/plans/counter-asic-3-reserve.md), the AMD
* port of proto-cuda/family-probe.cu and proto-metal/family-probe.swift, 6 October 2026. PC 1 job on the RX 9070 XT;
* the Mac runs it on Apple OpenCL for the C-form and emulated rows only (every vendor builtin fails to build there,
* which is reported as a row, not an error).
*
* Same method as the CUDA probe: a dependent chain of one op per step per lane, 1,048,576 lanes x 4,096 steps, best
* of N, device event time, bit-exact against a CPU reference on two whole 32-lane groups (lanes 0..31 and the last
* 32). Every chain has the dot4 probe's glue: acc = OP(acc, x, y); x = x * K + acc; y = rotl(y, 7) ^ (acc + s).
*
* A family is measured through every form AMD's OpenCL C might give it, each built on its own (a variant the
* platform cannot compile prints one build=failed row with the first line of the build log and the run goes on):
* path=native an explicit intrinsic or builtin: __builtin_amdgcn_ds_bpermute / ds_swizzle (the clang builtins the
* LC compiler accepts, the same path the 5 October dot4 row took through __builtin_amdgcn_sudot4),
* sub_group_shuffle[_xor] (cl_khr_subgroup_shuffle), intel_sub_group_shuffle[_xor]
* (cl_intel_subgroups), amd_bfe / amd_perm (cl_amd_media_ops2), __builtin_amdgcn_sudot4, the
* cl_khr_integer_dot_product dot(), and the WMMA builtins for mm8
* path=sequence the plain OpenCL C form (rotate, shifts, masks, popcount, clz, the ternary select): whatever
* instruction sequence the compiler emits; one instruction or several is not read from the ISA here
* (no disassembler in the job), it is inferred from the step cost against rotr
* path=emulated a form that is certainly several instructions: the byte permute by shifts and masks, the lane
* shuffles through __local memory and two barriers, the dot4 by four byte products
* The families (the names of counter-asic-3-reserve.md and the CUDA probe): alu (reference chain, 5 ops a step, no
* acc), rotr, shflx (lane XOR 8), shl, shr, bfe (bits 7..19 of y), andn, perm (bytes y.b1, y.b3, y.b0, y.b2), popc,
* clz (clz(0) = 32), sel (bit 5 of y ? x : acc), shfla (lane + 3 mod 32), dot4 (signed, dp4a.s32.s32 semantics),
* mm8 (one 16x16x16 int8 WMMA per step per lane: the fragment layout of RDNA 4's WMMA is not verified against a CPU
* reference here, so that row is exact=unverified and OWED for exactness; its step cost stands if it builds).
*
* Output: one line per variant
* RESULT FAMILY name=<family> ms=<best> gsteps=<G steps/s> ratio=<best / alu best> exact=yes|no|unverified
* path=native|sequence|emulated variant=<name> ns=<ns per step> vendor=<v> device="<name>"
* RESULT FAMILY name=<family> variant=<name> path=<p> build=failed log="<first line>"
* and, per family, the row the status file takes (the fastest exact variant, native before sequence before emulated):
* RESULT FAMILYBEST name=<family> ms=<best> gsteps=<x> ratio=<r> exact=<e> path=<p> variant=<name>
*
* Build (Mac, Apple OpenCL): cc -std=c99 -O2 -Wno-deprecated-declarations -o family-probe-cl family-probe.c -framework OpenCL
* Build (Windows, mingw, no SDK): x86_64-w64-mingw32-gcc -std=c99 -O2 -static -DIGNEUM_CL_DYNAMIC -DCL_TARGET_OPENCL_VERSION=120 \
* -I <redist>/include -o family-probe-cl.exe family-probe.c (OpenCL.dll loaded at run time)
* Run: family-probe-cl [--list] [--device N] [--lanes N] [--steps N] [--reps N] [--only name[,name]]
*/
#define CL_TARGET_OPENCL_VERSION 120
#ifdef __APPLE__
#include <OpenCL/cl.h>
#else
#include <CL/cl.h>
#endif
#ifdef IGNEUM_CL_DYNAMIC
#include "cl_dynamic.h"
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* ---- the kernel text: one COMMON block and one body per variant, every variant its own program ---- */
static const char* COMMON =
"static inline uint pm_mix(uint x) { x ^= x >> 16; x *= 0x7feb352du; x ^= x >> 15; x *= 0x846ca68bu; x ^= x >> 16; return x; }\n"
"static inline uint rotr_var(uint x, uint n) { n &= 31u; return (x >> n) | (x << ((32u - n) & 31u)); }\n"
"#define K32 0x9E3779B1u\n"
"#define CHAIN_HEAD uint g = (uint)get_global_id(0); uint lid = (uint)get_local_id(0); uint lane = lid & 31u; (void)lane; uint x = pm_mix(g ^ seed), y = x ^ 0x5bd1e995u; uint acc = pm_mix(x);\n"
"#define CHAIN_TAIL x = x * K32 + acc; y = rotate(y, 7u) ^ (acc + s);\n"
"#define CHAIN_OUT out[g] = acc ^ x ^ y;\n"
"#define CHAIN(OP) __kernel void probe(uint steps, uint seed, __global uint* out) { CHAIN_HEAD for (uint s = 0u; s < steps; ++s) { OP; CHAIN_TAIL } CHAIN_OUT }\n"
/* the lane id inside the wave on AMD (mbcnt of the full mask), so a ds_bpermute address stays inside the lane's
own 32-lane group whether the compiler chose wave32 or wave64 */
"#define AMD_WAVE_LANE (__builtin_amdgcn_mbcnt_hi(~0u, __builtin_amdgcn_mbcnt_lo(~0u, 0u)))\n";
static const char* K_ALU =
"__kernel void probe(uint steps, uint seed, __global uint* out) {\n"
" uint g = (uint)get_global_id(0); uint x = pm_mix(g ^ seed), y = x ^ 0x5bd1e995u;\n"
" for (uint s = 0u; s < steps; ++s) { x = x * K32 + rotate(y, 7u); y = (y ^ x) + s; }\n"
" out[g] = x ^ y;\n"
"}\n";
static const char* K_ROTR = "CHAIN(acc = rotr_var(y, x))\n";
static const char* K_SHL = "CHAIN(acc = y << (x & 31u))\n";
static const char* K_SHR = "CHAIN(acc = y >> (x & 31u))\n";
static const char* K_ANDN = "CHAIN(acc = y & ~x)\n";
static const char* K_POPC = "CHAIN(acc = acc + popcount(x))\n";
static const char* K_CLZ = "CHAIN(acc = acc + clz(x))\n";
static const char* K_SEL = "CHAIN(acc = ((y >> 5u) & 1u) ? x : acc)\n";
static const char* K_BFE_C = "CHAIN(acc = (y >> 7u) & 0x1fffu)\n";
static const char* K_BFE_AMD =
"#pragma OPENCL EXTENSION cl_amd_media_ops2 : enable\n"
"CHAIN(acc = amd_bfe(y, 7u, 13u))\n";
static const char* K_PERM_C =
"CHAIN(acc = ((y >> 8) & 0xffu) | (((y >> 24) & 0xffu) << 8) | ((y & 0xffu) << 16) | (((y >> 16) & 0xffu) << 24))\n";
/* v_perm_b32 through cl_amd_media_ops2: both sources are y, so the selector bytes 1, 3, 0, 2 pick y's bytes whichever
source the extension puts in the low half; the CPU check settles the byte order */
static const char* K_PERM_AMD =
"#pragma OPENCL EXTENSION cl_amd_media_ops2 : enable\n"
"CHAIN(acc = amd_perm(y, y, 0x02000301u))\n";
/* the lane shuffles */
static const char* K_SHFLX_BPERM =
"CHAIN(uint wl = AMD_WAVE_LANE; acc = acc ^ (uint)__builtin_amdgcn_ds_bpermute((int)((wl ^ 8u) << 2), (int)x))\n";
static const char* K_SHFLX_SWZ =
/* ds_swizzle bitmask mode: and_mask 0x1f, or_mask 0, xor_mask 8 -> offset 0x201f (lanes inside each 32-group) */
"CHAIN(acc = acc ^ (uint)__builtin_amdgcn_ds_swizzle((int)x, 0x201f))\n";
static const char* K_SHFLX_KHR =
"#pragma OPENCL EXTENSION cl_khr_subgroup_shuffle : enable\n"
"CHAIN(acc = acc ^ sub_group_shuffle_xor(x, 8u))\n";
static const char* K_SHFLX_INTEL =
"#pragma OPENCL EXTENSION cl_intel_subgroups : enable\n"
"CHAIN(acc = acc ^ intel_sub_group_shuffle_xor(x, 8u))\n";
static const char* K_SHFLX_LOCAL =
"__kernel void probe(uint steps, uint seed, __global uint* out) {\n"
" __local uint buf[256];\n"
" CHAIN_HEAD\n"
" for (uint s = 0u; s < steps; ++s) { buf[lid] = x; barrier(CLK_LOCAL_MEM_FENCE); uint v = buf[lid ^ 8u]; barrier(CLK_LOCAL_MEM_FENCE); acc = acc ^ v; CHAIN_TAIL }\n"
" CHAIN_OUT\n"
"}\n";
static const char* K_SHFLA_BPERM =
"CHAIN(uint wl = AMD_WAVE_LANE; acc = acc ^ (uint)__builtin_amdgcn_ds_bpermute((int)(((wl & ~31u) | ((wl + 3u) & 31u)) << 2), (int)x))\n";
static const char* K_SHFLA_KHR =
"#pragma OPENCL EXTENSION cl_khr_subgroup_shuffle : enable\n"
"CHAIN(uint sl = get_sub_group_local_id(); acc = acc ^ sub_group_shuffle(x, (sl & ~31u) | ((sl + 3u) & 31u)))\n";
static const char* K_SHFLA_INTEL =
"#pragma OPENCL EXTENSION cl_intel_subgroups : enable\n"
"CHAIN(uint sl = get_sub_group_local_id(); acc = acc ^ intel_sub_group_shuffle(x, (sl & ~31u) | ((sl + 3u) & 31u)))\n";
static const char* K_SHFLA_LOCAL =
"__kernel void probe(uint steps, uint seed, __global uint* out) {\n"
" __local uint buf[256];\n"
" CHAIN_HEAD\n"
" for (uint s = 0u; s < steps; ++s) { buf[lid] = x; barrier(CLK_LOCAL_MEM_FENCE); uint v = buf[(lid & ~31u) | ((lid + 3u) & 31u)]; barrier(CLK_LOCAL_MEM_FENCE); acc = acc ^ v; CHAIN_TAIL }\n"
" CHAIN_OUT\n"
"}\n";
/* dot4, signed bytes, wrapping accumulate (dp4a.s32.s32 semantics, the CUDA probe's dot4i row) */
static const char* K_DOT4_C =
"static inline uint dot4e(uint a, uint b, uint acc) { int4 va = convert_int4(as_char4(a)); int4 vb = convert_int4(as_char4(b)); return acc + (uint)(va.x * vb.x + va.y * vb.y + va.z * vb.z + va.w * vb.w); }\n"
"CHAIN(acc = dot4e(x, y, acc))\n";
static const char* K_DOT4_AMD =
"CHAIN(acc = (uint)__builtin_amdgcn_sudot4(true, (int)x, true, (int)y, (int)acc, false))\n";
static const char* K_DOT4_KHR =
"#pragma OPENCL EXTENSION cl_khr_integer_dot_product : enable\n"
"CHAIN(acc = acc + (uint)dot(as_char4(x), as_char4(y)))\n";
/* mm8: one 16x16x16 int8 WMMA per step per lane through the clang builtins (gfx12 first, the gfx11 shape second).
8 bytes of A and B per lane on gfx12 (int2), 16 (int4) on gfx11; 8 accumulators per lane (int8); the chain carries
c.s0 forward as acc. No CPU reference for the fragment layout here: exact=unverified. */
static const char* K_MM8_GFX12 =
"__kernel void probe(uint steps, uint seed, __global uint* out) {\n"
" CHAIN_HEAD int8 c = (int8)((int)acc, (int)pm_mix(acc), 0, 0, 0, 0, 0, 0);\n"
" for (uint s = 0u; s < steps; ++s) { int2 a = (int2)((int)x, (int)y); int2 b = (int2)((int)(y ^ s), (int)x);\n"
" c = __builtin_amdgcn_wmma_i32_16x16x16_iu8_w32_gfx12(true, a, true, b, c, false); acc = (uint)c.s0; CHAIN_TAIL }\n"
" CHAIN_OUT\n"
"}\n";
static const char* K_MM8_GFX11 =
"__kernel void probe(uint steps, uint seed, __global uint* out) {\n"
" CHAIN_HEAD int8 c = (int8)((int)acc, (int)pm_mix(acc), 0, 0, 0, 0, 0, 0);\n"
" for (uint s = 0u; s < steps; ++s) { int4 a = (int4)((int)x, (int)y, (int)(x ^ s), (int)(y + s)); int4 b = (int4)((int)(y ^ s), (int)x, (int)y, (int)x);\n"
" c = __builtin_amdgcn_wmma_i32_16x16x16_iu8_w32(true, a, true, b, c, false); acc = (uint)c.s0; CHAIN_TAIL }\n"
" CHAIN_OUT\n"
"}\n";
typedef struct { const char* family; const char* variant; const char* path; const char* body; int vendorOnly; } Variant;
/* vendorOnly: 0 = every platform, 1 = AMD builtins (clang), 2 = NVIDIA only (none here) */
static const Variant VARIANTS[] = {
{ "alu", "alu", "sequence", NULL, 0 },
{ "rotr", "rotr_c", "sequence", NULL, 0 },
{ "shflx", "shflx_bperm", "native", NULL, 1 },
{ "shflx", "shflx_swz", "native", NULL, 1 },
{ "shflx", "shflx_khr", "native", NULL, 0 },
{ "shflx", "shflx_intel", "native", NULL, 0 },
{ "shflx", "shflx_local", "emulated", NULL, 0 },
{ "shl", "shl_c", "sequence", NULL, 0 },
{ "shr", "shr_c", "sequence", NULL, 0 },
{ "bfe", "bfe_amd", "native", NULL, 0 },
{ "bfe", "bfe_c", "sequence", NULL, 0 },
{ "andn", "andn_c", "sequence", NULL, 0 },
{ "perm", "perm_amd", "native", NULL, 0 },
{ "perm", "perm_c", "emulated", NULL, 0 },
{ "popc", "popc_c", "sequence", NULL, 0 },
{ "clz", "clz_c", "sequence", NULL, 0 },
{ "sel", "sel_c", "sequence", NULL, 0 },
{ "shfla", "shfla_bperm", "native", NULL, 1 },
{ "shfla", "shfla_khr", "native", NULL, 0 },
{ "shfla", "shfla_intel", "native", NULL, 0 },
{ "shfla", "shfla_local", "emulated", NULL, 0 },
{ "dot4", "dot4_amd", "native", NULL, 1 },
{ "dot4", "dot4_khr", "native", NULL, 0 },
{ "dot4", "dot4_c", "emulated", NULL, 0 },
{ "mm8", "mm8_gfx12", "native", NULL, 1 },
{ "mm8", "mm8_gfx11", "native", NULL, 1 },
};
static const int NVARIANTS = (int)(sizeof(VARIANTS) / sizeof(VARIANTS[0]));
static const char* bodyOf(const char* variant) {
if (!strcmp(variant, "alu")) return K_ALU;
if (!strcmp(variant, "rotr_c")) return K_ROTR;
if (!strcmp(variant, "shflx_bperm")) return K_SHFLX_BPERM;
if (!strcmp(variant, "shflx_swz")) return K_SHFLX_SWZ;
if (!strcmp(variant, "shflx_khr")) return K_SHFLX_KHR;
if (!strcmp(variant, "shflx_intel")) return K_SHFLX_INTEL;
if (!strcmp(variant, "shflx_local")) return K_SHFLX_LOCAL;
if (!strcmp(variant, "shl_c")) return K_SHL;
if (!strcmp(variant, "shr_c")) return K_SHR;
if (!strcmp(variant, "bfe_amd")) return K_BFE_AMD;
if (!strcmp(variant, "bfe_c")) return K_BFE_C;
if (!strcmp(variant, "andn_c")) return K_ANDN;
if (!strcmp(variant, "perm_amd")) return K_PERM_AMD;
if (!strcmp(variant, "perm_c")) return K_PERM_C;
if (!strcmp(variant, "popc_c")) return K_POPC;
if (!strcmp(variant, "clz_c")) return K_CLZ;
if (!strcmp(variant, "sel_c")) return K_SEL;
if (!strcmp(variant, "shfla_bperm")) return K_SHFLA_BPERM;
if (!strcmp(variant, "shfla_khr")) return K_SHFLA_KHR;
if (!strcmp(variant, "shfla_intel")) return K_SHFLA_INTEL;
if (!strcmp(variant, "shfla_local")) return K_SHFLA_LOCAL;
if (!strcmp(variant, "dot4_amd")) return K_DOT4_AMD;
if (!strcmp(variant, "dot4_khr")) return K_DOT4_KHR;
if (!strcmp(variant, "dot4_c")) return K_DOT4_C;
if (!strcmp(variant, "mm8_gfx12")) return K_MM8_GFX12;
if (!strcmp(variant, "mm8_gfx11")) return K_MM8_GFX11;
return NULL;
}
/* ---- CPU reference: one whole 32-lane group (lanes g0 .. g0+31), the CUDA probe's warp_ref without mm8 ---- */
static uint32_t pm_mix(uint32_t x) { x ^= x >> 16; x *= 0x7feb352du; x ^= x >> 15; x *= 0x846ca68bu; x ^= x >> 16; return x; }
static uint32_t rotl32(uint32_t v, uint32_t n) { return (v << n) | (v >> (32u - n)); }
static uint32_t rotr_var(uint32_t x, uint32_t n) { n &= 31u; return (x >> n) | (x << ((32u - n) & 31u)); }
static uint32_t clz32(uint32_t x) { uint32_t n = 0; if (!x) return 32; while (!(x & 0x80000000u)) { x <<= 1; ++n; } return n; }
static uint32_t popc32(uint32_t x) { uint32_t n = 0; while (x) { n += x & 1u; x >>= 1; } return n; }
static uint32_t perm_ref(uint32_t y) { return ((y >> 8) & 0xffu) | (((y >> 24) & 0xffu) << 8) | ((y & 0xffu) << 16) | (((y >> 16) & 0xffu) << 24); }
static uint32_t dot4s_ref(uint32_t a, uint32_t b, uint32_t acc) {
int32_t r = (int32_t)acc; int i;
for (i = 0; i < 4; ++i) { int32_t ba = (int32_t)(int8_t)((a >> (8 * i)) & 0xffu); int32_t bb = (int32_t)(int8_t)((b >> (8 * i)) & 0xffu); r = (int32_t)((uint32_t)r + (uint32_t)(ba * bb)); }
return (uint32_t)r;
}
#define K32 0x9E3779B1u
static int group_ref(const char* family, uint32_t g0, uint32_t seed, uint32_t steps, uint32_t* res) {
uint32_t x[32], y[32], acc[32], xs[32], s; int l;
for (l = 0; l < 32; ++l) { x[l] = pm_mix((g0 + (uint32_t)l) ^ seed); y[l] = x[l] ^ 0x5bd1e995u; acc[l] = pm_mix(x[l]); }
if (!strcmp(family, "alu")) {
for (s = 0; s < steps; ++s) for (l = 0; l < 32; ++l) { x[l] = x[l] * K32 + rotl32(y[l], 7u); y[l] = (y[l] ^ x[l]) + s; }
for (l = 0; l < 32; ++l) res[l] = x[l] ^ y[l];
return 1;
}
if (!strcmp(family, "mm8")) return 0; /* no reference: the WMMA fragment layout is not modelled here */
for (s = 0; s < steps; ++s) {
memcpy(xs, x, sizeof xs);
for (l = 0; l < 32; ++l) {
uint32_t xv = xs[l], yv = y[l];
if (!strcmp(family, "rotr")) acc[l] = rotr_var(yv, xv);
else if (!strcmp(family, "shflx")) acc[l] = acc[l] ^ xs[l ^ 8];
else if (!strcmp(family, "shl")) acc[l] = yv << (xv & 31u);
else if (!strcmp(family, "shr")) acc[l] = yv >> (xv & 31u);
else if (!strcmp(family, "bfe")) acc[l] = (yv >> 7u) & 0x1fffu;
else if (!strcmp(family, "andn")) acc[l] = yv & ~xv;
else if (!strcmp(family, "perm")) acc[l] = perm_ref(yv);
else if (!strcmp(family, "popc")) acc[l] = acc[l] + popc32(xv);
else if (!strcmp(family, "clz")) acc[l] = acc[l] + clz32(xv);
else if (!strcmp(family, "sel")) acc[l] = ((yv >> 5u) & 1u) ? xv : acc[l];
else if (!strcmp(family, "shfla")) acc[l] = acc[l] ^ xs[(l + 3) & 31];
else if (!strcmp(family, "dot4")) acc[l] = dot4s_ref(xv, yv, acc[l]);
else { printf("no reference for %s\n", family); exit(3); }
x[l] = xv * K32 + acc[l];
y[l] = rotl32(yv, 7u) ^ (acc[l] + s);
}
}
for (l = 0; l < 32; ++l) res[l] = acc[l] ^ x[l] ^ y[l];
return 1;
}
/* ---- devices ---- */
typedef struct { cl_platform_id p; cl_device_id d; char pname[128], dname[128], driver[64], ver[64]; } Dev;
static Dev devs[32]; static int ndevs = 0;
static void enumerate(void) {
cl_platform_id ps[8]; cl_uint np = 0, i;
if (clGetPlatformIDs(8, ps, &np) != CL_SUCCESS) return;
for (i = 0; i < np; ++i) {
cl_device_id ds[8]; cl_uint nd = 0, j;
if (clGetDeviceIDs(ps[i], CL_DEVICE_TYPE_GPU, 8, ds, &nd) != CL_SUCCESS) continue;
for (j = 0; j < nd && ndevs < 32; ++j) {
Dev* v = &devs[ndevs++]; v->p = ps[i]; v->d = ds[j];
clGetPlatformInfo(ps[i], CL_PLATFORM_NAME, sizeof v->pname, v->pname, NULL);
clGetDeviceInfo(ds[j], CL_DEVICE_NAME, sizeof v->dname, v->dname, NULL);
clGetDeviceInfo(ds[j], CL_DRIVER_VERSION, sizeof v->driver, v->driver, NULL);
clGetDeviceInfo(ds[j], CL_DEVICE_VERSION, sizeof v->ver, v->ver, NULL);
}
}
}
typedef struct { double bestMs; int built; int exact; } Run; /* exact: 1 yes, 0 no, -1 unverified */
static Run run_variant(cl_context ctx, cl_command_queue q, cl_device_id dev, const Variant* v, cl_uint lanes, cl_uint steps, int reps,
cl_mem out, uint32_t* host, const char* vendor, const char* dname, double aluBest) {
const char* srcs[2]; cl_int err; cl_program prog; cl_kernel k; int r, exact = 1, hasRef = 1; double best = -1; Run res = { -1, 0, 0 };
srcs[0] = COMMON; srcs[1] = v->body;
prog = clCreateProgramWithSource(ctx, 2, srcs, NULL, &err);
if (err != CL_SUCCESS) { printf("RESULT FAMILY name=%s variant=%s path=%s build=failed log=\"clCreateProgramWithSource %d\"\n", v->family, v->variant, v->path, (int)err); return res; }
err = clBuildProgram(prog, 1, &dev, "-cl-std=CL1.2", NULL, NULL);
if (err != CL_SUCCESS) {
size_t n = 0; char* log; char* p; char* nl;
clGetProgramBuildInfo(prog, dev, CL_PROGRAM_BUILD_LOG, 0, NULL, &n); log = (char*)calloc(n + 2, 1);
if (n) clGetProgramBuildInfo(prog, dev, CL_PROGRAM_BUILD_LOG, n, log, NULL);
p = log; while (*p == '\n' || *p == '\r' || *p == ' ') ++p;
nl = strpbrk(p, "\r\n"); if (nl) *nl = 0;
for (nl = p; *nl; ++nl) if (*nl == '"') *nl = '\'';
printf("RESULT FAMILY name=%s variant=%s path=%s build=failed log=\"%.160s\"\n", v->family, v->variant, v->path, p);
free(log); clReleaseProgram(prog); return res;
}
k = clCreateKernel(prog, "probe", &err);
if (err != CL_SUCCESS) { printf("RESULT FAMILY name=%s variant=%s path=%s build=failed log=\"clCreateKernel %d\"\n", v->family, v->variant, v->path, (int)err); clReleaseProgram(prog); return res; }
for (r = 0; r < reps; ++r) {
cl_uint seed = 0x2468aceu + (cl_uint)r * 0x9E3779B9u; size_t global = lanes, local = 256; cl_event ev; cl_ulong t0, t1; double ms; uint32_t g0s[2]; int j;
clSetKernelArg(k, 0, sizeof(cl_uint), &steps); clSetKernelArg(k, 1, sizeof(cl_uint), &seed); clSetKernelArg(k, 2, sizeof(cl_mem), &out);
err = clEnqueueNDRangeKernel(q, k, 1, NULL, &global, &local, 0, NULL, &ev);
if (err != CL_SUCCESS) { printf("RESULT FAMILY name=%s variant=%s path=%s build=failed log=\"launch failed %d\"\n", v->family, v->variant, v->path, (int)err); clReleaseKernel(k); clReleaseProgram(prog); return res; }
clWaitForEvents(1, &ev);
clGetEventProfilingInfo(ev, CL_PROFILING_COMMAND_START, sizeof t0, &t0, NULL); clGetEventProfilingInfo(ev, CL_PROFILING_COMMAND_END, sizeof t1, &t1, NULL);
ms = (double)(t1 - t0) / 1e6; clReleaseEvent(ev);
if (best < 0 || ms < best) best = ms;
clEnqueueReadBuffer(q, out, CL_TRUE, 0, (size_t)lanes * 4, host, 0, NULL, NULL);
g0s[0] = 0; g0s[1] = lanes - 32u;
for (j = 0; j < 2; ++j) {
uint32_t want[32]; int l;
if (!group_ref(v->family, g0s[j], seed, steps, want)) { hasRef = 0; break; }
for (l = 0; l < 32; ++l) if (host[g0s[j] + (uint32_t)l] != want[l]) { exact = 0; printf("MISMATCH %s lane %u: gpu %08x cpu %08x\n", v->variant, g0s[j] + (uint32_t)l, host[g0s[j] + (uint32_t)l], want[l]); break; }
}
}
{
double sps = (double)lanes * (double)steps / (best / 1000.0);
double ratio = aluBest > 0 ? best / aluBest : 0;
const char* ex = hasRef ? (exact ? "yes" : "no") : "unverified";
printf("RESULT FAMILY name=%s ms=%.3f gsteps=%.2f ratio=%.3f exact=%s path=%s variant=%s ns=%.3f lanes=%u steps=%u vendor=%s device=\"%s\"\n",
v->family, best, sps / 1e9, ratio, ex, v->path, v->variant, best * 1e6 / (double)steps, lanes, steps, vendor, dname);
res.bestMs = best; res.built = 1; res.exact = hasRef ? exact : -1;
}
clReleaseKernel(k); clReleaseProgram(prog); return res;
}
static int pathRank(const char* p) { return !strcmp(p, "native") ? 0 : !strcmp(p, "sequence") ? 1 : 2; }
int main(int argc, char** argv) {
cl_uint lanes = 1u << 20, steps = 4096u; int reps = 3, device = 0, list = 0, i; Dev* v; cl_int err; cl_context ctx; cl_command_queue q; cl_mem out; uint32_t* host; const char* vendor; const char* only = NULL;
Run runs[64]; double aluBest = 0; int isAmd;
for (i = 1; i < argc; ++i) {
if (!strcmp(argv[i], "--list")) list = 1;
else if (!strcmp(argv[i], "--device") && i + 1 < argc) device = atoi(argv[++i]);
else if (!strcmp(argv[i], "--lanes") && i + 1 < argc) lanes = (cl_uint)strtoul(argv[++i], 0, 10);
else if (!strcmp(argv[i], "--steps") && i + 1 < argc) steps = (cl_uint)strtoul(argv[++i], 0, 10);
else if (!strcmp(argv[i], "--reps") && i + 1 < argc) reps = atoi(argv[++i]);
else if (!strcmp(argv[i], "--only") && i + 1 < argc) only = argv[++i];
else { printf("unknown argument %s\n", argv[i]); return 2; }
}
if (lanes < 64 || (lanes & 255u)) { printf("--lanes must be a multiple of 256\n"); return 2; }
#ifdef IGNEUM_CL_DYNAMIC
if (!ig_cl_load()) { printf("%s\n", ig_cl_error); return 1; }
#endif
enumerate();
if (list || ndevs == 0) { for (i = 0; i < ndevs; ++i) printf("[%d] %s | %s | driver %s | %s\n", i, devs[i].dname, devs[i].pname, devs[i].driver, devs[i].ver); if (ndevs == 0) printf("no OpenCL GPU devices\n"); return ndevs ? 0 : 1; }
if (device < 0 || device >= ndevs) { printf("no device %d (have %d)\n", device, ndevs); return 2; }
v = &devs[device];
vendor = strstr(v->pname, "NVIDIA") ? "nvidia" : (strstr(v->pname, "AMD") ? "amd" : (strstr(v->pname, "Apple") ? "apple" : "other"));
isAmd = !strcmp(vendor, "amd");
ctx = clCreateContext(NULL, 1, &v->d, NULL, NULL, &err); if (err != CL_SUCCESS) { printf("clCreateContext %d\n", (int)err); return 1; }
q = clCreateCommandQueue(ctx, v->d, CL_QUEUE_PROFILING_ENABLE, &err); if (err != CL_SUCCESS) { printf("clCreateCommandQueue %d\n", (int)err); return 1; }
out = clCreateBuffer(ctx, CL_MEM_READ_WRITE, (size_t)lanes * 4, NULL, &err); if (err != CL_SUCCESS) { printf("clCreateBuffer %d\n", (int)err); return 1; }
host = (uint32_t*)malloc((size_t)lanes * 4);
printf("family-probe (OpenCL) on [%d] %s | %s | driver %s | %s, lanes %u, steps %u, best of %d, device event time\n", device, v->dname, v->pname, v->driver, v->ver, lanes, steps, reps);
{
char ext[16384]; ext[0] = 0; clGetDeviceInfo(v->d, CL_DEVICE_EXTENSIONS, sizeof ext, ext, NULL);
printf("RESULT EXT device=\"%s\" khr_subgroups=%s khr_subgroup_shuffle=%s intel_subgroups=%s amd_media_ops2=%s khr_integer_dot_product=%s\n", v->dname,
strstr(ext, "cl_khr_subgroups") ? "yes" : "no", strstr(ext, "cl_khr_subgroup_shuffle") ? "yes" : "no", strstr(ext, "cl_intel_subgroups") ? "yes" : "no",
strstr(ext, "cl_amd_media_ops2") ? "yes" : "no", strstr(ext, "cl_khr_integer_dot_product") ? "yes" : "no");
}
for (i = 0; i < NVARIANTS; ++i) {
Variant vv = VARIANTS[i]; vv.body = bodyOf(vv.variant);
runs[i].bestMs = -1; runs[i].built = 0; runs[i].exact = 0;
if (!vv.body) continue;
if (only && !strstr(only, vv.family)) continue;
if (vv.vendorOnly == 1 && !isAmd) { printf("RESULT FAMILY name=%s variant=%s path=%s build=skipped log=\"AMD clang builtin, platform is %s\"\n", vv.family, vv.variant, vv.path, vendor); continue; }
runs[i] = run_variant(ctx, q, v->d, &vv, lanes, steps, reps, out, host, vendor, v->dname, aluBest);
if (i == 0 && runs[i].built) aluBest = runs[i].bestMs;
}
/* the row per family: the fastest EXACT variant, native before sequence before emulated at equal exactness; a
family with no exact variant takes its fastest unverified one and says so */
for (i = 0; i < NVARIANTS; ++i) {
int j, bestIdx = -1, bestRank = 9; double bestMs = 1e30;
if (i > 0 && !strcmp(VARIANTS[i].family, VARIANTS[i - 1].family)) continue; /* one row per family, at its first variant */
if (only && !strstr(only, VARIANTS[i].family)) continue;
for (j = i; j < NVARIANTS && !strcmp(VARIANTS[j].family, VARIANTS[i].family); ++j) {
int rank;
if (!runs[j].built) continue;
rank = (runs[j].exact == 1 ? 0 : runs[j].exact == -1 ? 3 : 6) + pathRank(VARIANTS[j].path);
if (rank < bestRank || (rank == bestRank && runs[j].bestMs < bestMs)) { bestRank = rank; bestMs = runs[j].bestMs; bestIdx = j; }
}
if (bestIdx < 0) { printf("RESULT FAMILYBEST name=%s built=none\n", VARIANTS[i].family); continue; }
printf("RESULT FAMILYBEST name=%s ms=%.3f gsteps=%.2f ratio=%.3f exact=%s path=%s variant=%s\n", VARIANTS[i].family, runs[bestIdx].bestMs,
(double)lanes * (double)steps / (runs[bestIdx].bestMs / 1000.0) / 1e9, aluBest > 0 ? runs[bestIdx].bestMs / aluBest : 0,
runs[bestIdx].exact == 1 ? "yes" : runs[bestIdx].exact == -1 ? "unverified" : "no", VARIANTS[bestIdx].path, VARIANTS[bestIdx].variant);
}
clReleaseMemObject(out); clReleaseCommandQueue(q); clReleaseContext(ctx); free(host);
printf("family-probe: done\n");
return 0;
}

View file

@ -510,6 +510,7 @@ static const char* exchangeName(int m) { return m == 1 ? "sub_group_shuffle_xor
// Returns 0 on success, 1 on build failure (log printed).
static int buildProgram(Device* dv, const DeviceInfo* di, const char* src, size_t srcLen, int exchangeMode, int groupSize, const char* extra) {
cl_int err = 0;
double tb = wallMs(); /* the pack's compile cost (Counter ASIC 3.0 item 2, 6 October 2026): printed as one line below */
const char* std;
// The sub-group built-ins need OpenCL C 2.0 or 3.0. OpenCL 3.0 devices may report "OpenCL C 1.2" as the default
// CL_DEVICE_OPENCL_C_VERSION while supporting 3.0 (the 3.0 API lists all versions; the 1.2 API cannot ask), so
@ -534,6 +535,10 @@ static int buildProgram(Device* dv, const DeviceInfo* di, const char* src, size_
clReleaseProgram(dv->prog); dv->prog = NULL;
return 1;
}
/* The OpenCL build time of this pack's kernel text (clBuildProgram alone), the equivalent of the CUDA worker's
* NVRTC line: a per-day item-derivation program (item 2) sits inside every hash-kernel compile, so its cost is
* read here. Wall time, printed before the kernels are created. */
printf("build %.1f ms clBuildProgram (exchange %d, group %d)\n", wallMs() - tb, exchangeMode, groupSize);
dv->kHash = clCreateKernel(dv->prog, "igneum_hash", &err); CL_CHECK_ERR(err, "clCreateKernel igneum_hash");
dv->kHashBound = clCreateKernel(dv->prog, "igneum_hash_bound", &err);
if (err != CL_SUCCESS) dv->kHashBound = NULL; /* kernel.cl without the bound kernel: fine outside --serve */

View file

@ -0,0 +1,74 @@
# Counter ASIC 3.0: the PC 1 AMD jobs (6 October 2026)
Four signed `run` jobs for PC 1 (machine `ae432dc7`: RTX 5090, RTX 4070, RX 9070 XT `amd:gfx1201` on the eGPU, all on the installed app) that fill the OWED AMD rows of `docs/plans/counter-asic-3-status.md` sections 3, 5 and 7, plus the 4070 row of item 8. Prepared on branch `ca3-pc1-amd`; NOTHING here is published by this branch. The coordinator publishes on "go PC 1 AMD", one job at a time, in the order below, and reads each closing report before the next (`node tools/jobs.mjs <id>`, `node tools/jobs.mjs watch <id>`).
## Rules every script keeps
| Rule | How |
|---|---|
| One job at a time on PC 1 | the publish order below; each job's closing report read before the next |
| The card alone only where the fixture needs it | job 1 and job 4 switch ONLY the card under test off through `POST <app.url>api/cards` (plain `run` jobs, not `--stop-miners`: the other two cards keep mining); jobs 2 and 3 run beside the miners and print `card_state=loaded|quiet` on every row |
| Card keys from settings.json, never `/api/state`, BOTH forms | every form is posted: the key as written (`amd:1:gfx1201`, `nvidia:1:NVIDIA GeForce RTX 4070` or whatever the file holds), `vendor:index`, `vendor:code`; `apply_cards` (engine.rs) skips a key that matches no live card, so the extra forms are harmless and nothing is written for them |
| Quiet confirmed by the process list | the worker process carrying the card's `--device <index>` must be gone (Win32_Process command lines; job 4 also reads `nvidia-smi -i <idx> --query-compute-apps`); an unconfirmed card prints `UNCONFIRMED` and the rows say `card_alone=False` |
| Restored in `finally` | the card posted back enabled (only when settings.json had it enabled), the worker's return polled for 90 s and printed; the sampler ended by its own pid (never by name: the app runs its own copy of the AMD helper) |
| The installed app untouched | no `/api/quit`, `/api/pause`, `/api/resume`, no manifest, no settings.json write; the only writes are POST `api/cards` on the card under test |
| Copied sources re-stamped | nothing is built on the PC by these jobs (no cargo; the OpenCL kernels are compiled by the driver at run time); `tools/ci/copied-sources-check.sh` passes |
| Prover socket | not applicable (no prover host in these jobs) |
| Every failure a `RESULT ... error=<text>` line, a `SUMMARY {json}` line at the end | all four scripts |
If `--stop-miners` IS added at publish time, jobs 1 and 4 see the card already quiet, say so (`card already quiet before the switch`) and skip the switch: the scripts work either way.
## The kit (one fetch job, before job 1)
`tools/ca3-pc1-amd/make-kit.sh` builds `igneum-ca3-pc1-amd-kit.zip`: `bin/igneum-worker-opencl.exe` (THIS tree's `proto-opencl/host.c`, the installed worker's source plus one `build <ms> clBuildProgram` line, cross-compiled with mingw as `proto-cuda/nvrtc/build-windows.sh` does), `bin/family-probe-cl.exe` (`proto-opencl/family-probe.c`), `src/` (both sources and `cl_dynamic.h` for the sha256 record), `packs/` (mx8-devnet-epoch0, sh256x13, sh256x27, sh256x53, sh256x88, sh64x52, dr736-genesis, dr736-devnet-epoch0, each with its kernel_bound.cl, program.json and vectors), `SHA256SUMS`.
Built 6 October 2026, 15:4x UTC (`IGNEUM_REDIST=/Users/joshm/Projects/igneum-wt-ca2-mixer/proto-cuda/nvrtc/redist`): zip sha256 `a70fce5be672f33c3af08a5699488c09aef2848010f468ec99165fd4ab9ca61f`, 884,381 bytes, 113 files; `bin/igneum-worker-opencl.exe` `a572948e86c23317a80d9d7bb9e4bf70d5f7dfc40470974e2f08da1892016ac6`, `bin/family-probe-cl.exe` `d51a36bcae13ab36c40c2f347e3bdbe28f010499ccf84e423ecc04a9b4f151e3`, `src/host.c` `5e23ac94…`, `src/family-probe.c` `c293be9d…`. The zip is in the session scratchpad (`pc1amd/igneum-ca3-pc1-amd-kit.zip`), not in the repository; re-running `make-kit.sh` rebuilds it and prints a new sha256 (mingw output is not byte-reproducible), and the publish line takes the printed one. Every job prints `RESULT kitfile <path> sha256 <hex>` for the exes and sources it used, so the record closes on the PC side.
```
tools/ca3-pc1-amd/make-kit.sh "$TMPDIR/igneum-ca3-pc1-amd-kit.zip"
packaging/ota/publish-jobs.sh add --kind fetch --target ae432dc7 --id fetch-ca3-pc1-amd-20261006 \
--file "$TMPDIR/igneum-ca3-pc1-amd-kit.zip" --dir jobs --extract --title "CA3 PC 1 AMD kit" --expires-hours 48 --deploy
```
The fetch lands at `<jobs>\fetch-ca3-pc1-amd-20261006\{bin,packs,src,SHA256SUMS}` (jobrun.rs `fetch_base`: the job id under the jobs folder). The wiped-jobs-folder class: an app update clears the jobs folder, so if the 0.3.12 install lands on PC 1 between jobs, republish the fetch (each run job tests the kit before use and fails in seconds with `kit missing` if it is gone).
## The jobs, in order
| # | Script | Publish (after the fetch; `--id` fixed so the read-back is one command) | Length | Card | Rows it fills |
|---|---|---|---|---|---|
| 1 | `pc1-amd-g1-shadow.ps1` | `packaging/ota/publish-jobs.sh add --kind run --target ae432dc7 --id run-ca3-pc1-amd-g1-shadow-20261006 --script tools/ca3-pc1-amd/pc1-amd-g1-shadow.ps1 --timeout-minutes 40 --title "CA3 PC 1: G1 + AMD ladder on the 9070 XT" --deploy` | about 15 min (7 benches of 60 to 90 dispatches of 2^24 at 0.9 to 2 s each, the card switch up to 150 s, the installed-worker cross-check 40 dispatches) | 9070 XT ALONE (its worker stopped; 5090 and 4070 keep mining) | G1 for `mx8+sh256x27` on the AMD vendor (section 5: `RESULT G1 pack=sh256x27 fingerprint=3d2e8245cc084d07 match=yes` turns "AMD vendor NOT RUN" green; the other five packs give the same for the ladder); the 9070 XT column of the item 8 tables (section 3: `RESULT LADDER pack=<p> ops=<N> mhs= watts= uj= gclk_mhz=`), where the card leaves the latency bound by the 5 percent rule; item 1's AMD per-joule row from the control (`RESULT LADDER pack=mx8-devnet-epoch0 ... uj=`: section 3 "Consequences per tier (item 1)", 16 GB AMD row); section 7 rows "item 8: the 9070 XT rows", "item 1: the 9070 XT rows" |
| 1b (the watts re-run) | `pc1-amd-watts.ps1` | `packaging/ota/publish-jobs.sh add --kind run --target ae432dc7 --id run-ca3-pc1-amd-watts-20261006 --script tools/ca3-pc1-amd/pc1-amd-watts.ps1 --timeout-minutes 20 --title "CA3 PC 1: watts on the 9070 XT (control + sh256x27)" --deploy` | about 5 min (2 x 100 dispatches at the 18.9 to 19.3 MH/s job 1 measured = 87 to 89 s each, the switch, 12 s of proof) | 9070 XT ALONE | the two `watts= uj=` cells job 1 left owed: item 1's AMD per-joule row (the control) and the candidate's AMD row in the item 8 table (sh256x27); the sampler is proven on the 12 idle seconds first (`RESULT sampler_raw` x3, `RESULT sampler_idle lines= lines_9070_with_watts=`) and the benches do not run without a 9070 XT watts line (`RESULT watts error=<why>`, status failed, card restored) |
| 2 | `pc1-amd-family.ps1` | `packaging/ota/publish-jobs.sh add --kind run --target ae432dc7 --id run-ca3-pc1-amd-family-20261006 --script tools/ca3-pc1-amd/pc1-amd-family.ps1 --timeout-minutes 15 --title "CA3 PC 1: family step costs on the 9070 XT" --deploy` | about 3 min (3 runs x up to 3 AMD devices, 26 variants each, builds dominate) | beside the miners (ratios; every row says `card_state=`) | the "RX 9070 XT" column of the item 6 table (section 3: `RESULT FAMILYBEST name=<f> ratio=<r> exact= path=` per family, `RESULT FAMILY ... variant=` for the form each took: `shfla_bperm` = `ds_bpermute_b32`, the one number that could move R3; `shflx_swz` / `shflx_bperm`; `perm_amd` = `v_perm_b32` through `amd_perm` or `perm_c` emulated; `bfe_amd`; `dot4_amd` = the 5 October sudot4 row re-measured; `mm8_gfx12` / `mm8_gfx11` = the WMMA builtins, exact=unverified); section 7 row "item 6: the 9070 XT step costs" |
| 3 | `pc1-amd-derive.ps1` | `packaging/ota/publish-jobs.sh add --kind run --target ae432dc7 --id run-ca3-pc1-amd-derive-20261006 --script tools/ca3-pc1-amd/pc1-amd-derive.ps1 --timeout-minutes 15 --title "CA3 PC 1: dr736 on the 9070 XT" --deploy` | about 4 min (3 packs x a 60-dispatch pass and a 3-dispatch pass) | beside the miners (build, compile and bit-exact rows; the rate as a ratio to mx8 in the same job) | the item 2 table's 9070 XT cells (section 3: `RESULT DERIVE pack=dr736-genesis build_ms= build_ms_cached= build_ms_over_mx8= dataset_ms= fingerprint= match= mhs= ratio_to_mx8=`): the OpenCL compile cost of the derivation program against mx8's (the NVRTC +1.1 s finding on AMD), the 1 GiB daily build against mx8's 72 to 77 ms (5 October), bit-exactness on the AMD vendor; section 7 row "item 2: the 9070 XT rows" |
| 4 (optional, confirmed by main) | `pc1-4070-shadow.ps1` | `packaging/ota/publish-jobs.sh add --kind run --target ae432dc7 --id run-ca3-pc1-4070-shadow-20261006 --script tools/ca3-pc1-amd/pc1-4070-shadow.ps1 --timeout-minutes 30 --title "CA3 PC 1: item 8 ladder on the RTX 4070" --deploy` | about 11 min (7 benches of 100 to 200 dispatches at an expected 0.4 to 1 s each, approximate: no 4070 row exists yet; NVRTC per pack; the card switch) | 4070 ALONE (its CUDA worker stopped; 5090 and 9070 XT keep mining) | the "small NVIDIA card binds near 100,000" row of item 8 (section 3, consequences per tier at N = 100,000; section 7 "the 4060-class rows"): `RESULT LADDER pack=<p> ops=<N> mhs= watts= uj= sm_mhz=` with nvidia-smi at 1 Hz on that index; G1 fingerprints on a second NVIDIA card |
Read back: `node tools/jobs.mjs run-ca3-pc1-amd-g1-shadow-20261006` (and `--all` for the 5-minute progress uploads). The run_id on the intake is `job-<id>-ae432dc7`.
## Job 1's first run (run-ca3-pc1-amd-g1-shadow-20261006, 15:46:27 to 15:56:54Z, exit 0) and what it found
G1 on the AMD vendor 7 of 7 (every fingerprint equal to the Mac's and the 5090's, self-test PASS, 96 of 96 lanes), the card to itself (worker pid 7060 gone after 5 s, back as pid 4900 after the restore), the ladder's rates on the kit worker (the installed worker's cross-check on the control 18.922 against 18.923 MH/s): 18.92 MH/s at 930 ops, 19.31 at 49,700, 19.29 at 102,100, 19.11 at 199,600, 19.60 at 330,700, 19.07 for the 64-instruction block at 49,700; OpenCL build 42 to 51 ms for mx8, 418 to 429 ms for the 256-instruction shadow packs, 342 for the 64-instruction one; the daily build 70 to 74 ms. Two faults, both fixed on this branch (commit after e054ed7):
| Fault | Cause | Fix |
|---|---|---|
| every LADDER row `watts=owed samples=0` though the one-shot helper call printed the 9070 XT at 140.0 W | `Start-Process powershell.exe -ArgumentList @(..., $tele, $samples)`: Windows PowerShell 5.1 joins an ArgumentList with spaces and does not quote, and the helper's path holds "Igneum Miner", so the wrapper's `$Exe` was `...\Programs\Igneum`, its `& $Exe -l 1` failed and nothing was written | the wrapper reads its paths from `IGNEUM_SAMPLER_EXE` and `IGNEUM_SAMPLER_OUT` in the environment (inherited by the child, never on a command line), the `-File` path is quoted, the wrapper logs its own start line with `exists=`; the sampler must prove itself in the 12 idle seconds (first three raw lines printed, a 9070 XT line with watts required) or the benches are not run and the job fails loudly with `RESULT watts error=<why>` |
| the restore posted one entry's identities (2) to every key form while settings.json holds three gfx1201 entries (`amd:3:gfx1201` off / 2, `amd:gfx1201` on / 8, `amd:1:gfx1201` on / 2); the worker came back (pid 4900), but if the app's live key is `amd:gfx1201` its identities moved from 8 to 2 | one `$cardIdent` for every form | every entry is restored with ITS OWN enabled flag and identities (and cap, job 4), the derived forms first and the key as written last so an entry's own settings win on the live key; job 4 carries the same shape. OWED: a read of PC 1's settings.json (or the app's card tile) to confirm which gfx1201 key the app runs and whether its identities read 8 or 2 after the 15:56Z restore; if 2 and the app runs `amd:gfx1201`, one `POST api/cards {key: amd:gfx1201, enabled: true, identities: 8}` puts it back (the coordinator's call, not done by any job here) |
## The AMD watts readback on PC 1 (what exists)
`igneum-gpu-telemetry.exe` (proto-opencl/gpu-telemetry.c on master, shipped by `packaging/windows/make-payload.sh` since 0.3.10; the Ember Tune playbook found it in the install folder or under a `jobs\amd-kit-*\kit\` folder) prints one line per AMD card per sample: `amd <n> bus <pci> kind discrete name "AMD Radeon RX 9070 XT" watts <W> temp_c <C> fan_rpm <r> fan_pct <p> mclk_mhz <m> gclk_mhz <g> util_pct <u> source adlx`, then `end <ms> N card(s)`, at `-l 1` once a second with `fflush` per sample. The watts field is ADLX `GPUPower` with `GPUTotalBoardPower` as the fallback (gpu-telemetry.c lines 120 to 121 on master); on 5 October it read the 9070 XT at 198.9 W at 17.73 MH/s (release-0.3.10.md, job `tele-measure-1`), the figure the app's MH per watt line uses. So a board-watts readback EXISTS and job 1 uses it: a second copy of the helper at 1 Hz through a wrapper that stamps each line (UTC, ms) so the samples window onto each bench (after its first 12 s, before its last 2 s, the PC 2 shape), ended by the wrapper's pid in `finally`. The job prints `RESULT tele helper=<path> sha256=<hex> watts_source=adlx` after one sample; if the helper is missing or prints `watts -` for the 9070 XT the rows carry `watts=owed uj=owed` and the SUMMARY says `watts: owed`, never a guess. Whether ADLX `GPUPower` on RDNA 4 is total board power or ASIC power is not verified here: the row says `adlx` and the number is what the app itself reports.
## What the Mac tested (6 October 2026, 15:27 to 15:28 UTC, `with-lock.sh run`, load average 3.9 at the start)
| Check | Result |
|---|---|
| `proto-opencl/family-probe.c` on Apple OpenCL (`cc -framework OpenCL`, M5 Max) | every C-form and emulated row bit-exact on both 32-lane groups in 3 repetitions; ratios to the alu chain beside the Metal probe's (rotr 1.14 against Metal 1.13, shl 0.89 / 0.85, shr 0.89 / 0.86, bfe 0.82 / 0.77, andn 0.84 / 0.75, popc 1.00 / 0.87, clz 1.11 / 1.01, sel 0.82 / 0.76, perm emulated 1.52 / 1.13, shflx and shfla through `__local` 1.96 and 2.04 (Metal native 0.86 and 1.91), dot4 emulated signed 4.66 / 4.73); the Apple `dot(char4,char4)` row exact=no as the 5 October finding said; every AMD builtin skipped (`build=skipped`), the khr, intel and amd_media_ops2 forms `build=failed` with the log line, the run went on; `FAMILYBEST` picked the exact rows |
| `proto-opencl/host.c` with the build line, `--bench-pack` on all eight kit packs on Apple OpenCL | self-test PASS (96 of 96 lanes) and the 2^24 fingerprint at base 0 equal to the recorded one on every pack: mx8-devnet-epoch0 `90f794dd556f7a3b`, sh256x13 `59ac286fe2a5a9ef`, sh256x27 `3d2e8245cc084d07`, sh256x53 `4f824b15cf2b124a`, sh256x88 `0572522e39a94d8a`, sh64x52 `9dd010f79d8ca9f4`, dr736-genesis `50e3eaa779da4f1e`, dr736-devnet-epoch0 `9553f6d5c667205a`; `build` line: mx8 70 ms, shadow packs 70 to 88 ms, dr736 337 to 356 ms (+270 ms on Apple OpenCL: item 2's compile cost shows on this harness too) |
| mingw cross-builds of both exes | clean (`-Wall -Wextra`), 116 KB and 35 KB |
| `tools/ci/bash-body-check.sh` on the five scripts | 0 bash bodies, all parse |
| `tools/ci/kit-path-check.sh` on the five scripts | 5 of 5 kit paths checked before use |
| `tools/ci/prover-socket-check.sh`, `copied-sources-check.sh`, `no-conflict-markers.sh` | pass |
| The Windows PowerShell 5.1 parse (`windows.yml` parse job) | the real parser runs only on the Windows runner and only over `relay/playbooks` and the other listed folders (not `tools/`); on the Mac the same rule's approximation (`tools/ci/check-workflow-shell.mjs`'s drive-reference regex, applied to these five files by hand) fired once on `"$dev: --stop-miners"` and the line was fixed to `${dev}:`; 0 findings after; brace, paren and bracket depth 0 by the bash-body-check tokenizer |
## Not tested on the Mac (the PC side)
The PowerShell itself never ran (no PowerShell on this Mac): the `api/cards` switch and its process-list confirmation, the wrapper sampler, `Get-CimInstance` command-line matching, the AMD helper's presence and its watts on the 9070 XT, the AMD OpenCL compiler's answer to each builtin (`ds_bpermute`, `ds_swizzle`, `amd_perm`, `amd_bfe`, the WMMA builtins), the driver's kernel cache (pass 2 of job 3), the 4070's nvidia-smi index against the app's `--device`, and the lengths above (estimates from the PC 2 runs and the 5 October 9070 XT rates). A failure of any of those prints a `RESULT ... error=` line and the SUMMARY carries `failed` or `partial`; the card restore in `finally` does not depend on them.

43
tools/ca3-pc1-amd/make-kit.sh Executable file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Builds the PC 1 AMD kit for the Counter ASIC 3.0 jobs in this folder (6 October 2026): the OpenCL worker from
# THIS tree's proto-opencl/host.c (the installed worker's source plus the `build <ms> clBuildProgram` line), the
# OpenCL family probe (proto-opencl/family-probe.c), both cross-compiled with mingw exactly as
# proto-cuda/nvrtc/build-windows.sh builds the shipped worker (static, IGNEUM_CL_DYNAMIC, OpenCL.dll at run time;
# no icon or version block: this is a bench kit, not a shipped exe), the eight packs the four jobs read, the two
# sources for the sha256 record, and SHA256SUMS. Prints the zip's sha256 and size: the README's publish line takes it.
# tools/ca3-pc1-amd/make-kit.sh [out.zip] default $TMPDIR/igneum-ca3-pc1-amd-kit.zip
# Needs x86_64-w64-mingw32-gcc (brew install mingw-w64) and the Khronos OpenCL headers at $IGNEUM_REDIST/include
# (default proto-cuda/nvrtc/redist, written by proto-cuda/nvrtc/fetch-redist.sh; the ca2-mixer worktree holds a copy:
# IGNEUM_REDIST=/Users/joshm/Projects/igneum-wt-ca2-mixer/proto-cuda/nvrtc/redist). Nothing third-party is committed.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
OUT="${1:-${TMPDIR:-/tmp}/igneum-ca3-pc1-amd-kit.zip}"
RED="${IGNEUM_REDIST:-$ROOT/proto-cuda/nvrtc/redist}"
[ -f "$RED/include/CL/cl.h" ] || { echo "no OpenCL headers at $RED/include/CL/cl.h (run proto-cuda/nvrtc/fetch-redist.sh or set IGNEUM_REDIST)" >&2; exit 1; }
CC=x86_64-w64-mingw32-gcc; STRIP=x86_64-w64-mingw32-strip
command -v "$CC" >/dev/null || { echo "$CC not found (brew install mingw-w64)" >&2; exit 1; }
PLACEHOLDER="$ROOT/proto-cuda/packs/igneum-devnet-v4-epoch0" # the memory-hard placeholder build-windows.sh uses; --bench-pack reads the real pack at run time
STAGE="$(mktemp -d)"
mkdir -p "$STAGE/bin" "$STAGE/src" "$STAGE/packs"
echo "== igneum-worker-opencl.exe (kit build of proto-opencl/host.c)"
"$CC" -std=c99 -O2 -Wall -Wextra -Wno-stringop-truncation -Wno-format-truncation -static -DIGNEUM_CL_DYNAMIC -DCL_TARGET_OPENCL_VERSION=120 \
-I "$RED/include" -I "$PLACEHOLDER" -DIGNEUM_KERNEL_PATH='"kernel_bound.cl"' -o "$STAGE/bin/igneum-worker-opencl.exe" "$ROOT/proto-opencl/host.c"
"$STRIP" "$STAGE/bin/igneum-worker-opencl.exe"
echo "== family-probe-cl.exe (proto-opencl/family-probe.c)"
"$CC" -std=c99 -O2 -Wall -Wextra -static -DIGNEUM_CL_DYNAMIC -DCL_TARGET_OPENCL_VERSION=120 -I "$RED/include" -o "$STAGE/bin/family-probe-cl.exe" "$ROOT/proto-opencl/family-probe.c"
"$STRIP" "$STAGE/bin/family-probe-cl.exe"
cp "$ROOT/proto-opencl/host.c" "$ROOT/proto-opencl/family-probe.c" "$ROOT/proto-opencl/cl_dynamic.h" "$STAGE/src/"
for p in packs-ca2-mixer/mx8-devnet-epoch0 packs-ca3-shadow/sh256x13 packs-ca3-shadow/sh256x27 packs-ca3-shadow/sh256x53 packs-ca3-shadow/sh256x88 packs-ca3-shadow/sh64x52 packs-ca3-derive/dr736-genesis packs-ca3-derive/dr736-devnet-epoch0; do
name="$(basename "$p")"
[ -f "$ROOT/proto-cuda/$p/kernel_bound.cl" ] || { echo "pack $p has no kernel_bound.cl" >&2; exit 1; }
mkdir -p "$STAGE/packs/$name"
cp "$ROOT/proto-cuda/$p"/* "$STAGE/packs/$name/"
done
( cd "$STAGE" && find . -type f ! -name SHA256SUMS | sort | xargs shasum -a 256 > SHA256SUMS )
rm -f "$OUT"
( cd "$STAGE" && zip -X -q -r "$OUT" . )
rm -rf "$STAGE"
echo "kit $OUT"
echo "sha256 $(shasum -a 256 "$OUT" | cut -c1-64) bytes $(wc -c < "$OUT" | tr -d ' ')"
unzip -l "$OUT" | tail -1

View file

@ -0,0 +1,215 @@
# Counter ASIC 3.0, PC 1 job 4 (6 October 2026, optional, confirmed by main at 16:xx UTC): item 8's ladder on PC 1's
# RTX 4070 (machine ae432dc7) through the INSTALLED igneum-worker-cuda.exe (NVRTC, the pack's own kernel text, the
# same harness as the PC 2 job tools/ca3-shadow/pc2-shadow-bench.ps1), nvidia-smi at 1 Hz on that card alone
# (power.draw, clocks.sm, clocks.mem, utilization, temperature): the "small NVIDIA card binds near 100,000 ops" row of
# the status file (section 3 "Item 8", section 7). Published as a plain signed `run` job (NOT --stop-miners): the
# RTX 5090 and the RX 9070 XT keep mining; this script switches ONLY the 4070 off in the installed app through POST
# <app.url>api/cards with its key read from settings.json (never api/state) in every form (as written, vendor:index,
# vendor:name; the 6 October finding), confirms by the process list that the CUDA worker carrying the 4070's --device
# index is gone and by nvidia-smi's compute-apps list on that index, benches, and restores the card in a finally block
# whatever happens. It never quits, pauses, resumes or updates the installed app, never writes settings.json, sets no
# power cap and no clock (nothing elevated). The 4070's nvidia-smi index is read from nvidia-smi by name; the app
# passes that index as the worker's --device (detect.rs), and the worker's own RESULT line names the device it ran on,
# which the script checks for "4070" before it takes a row. Packs: mx8-devnet-epoch0 (the class v3 control, first and
# last), sh256x13, sh256x27, sh256x53, sh256x88, sh64x52. Lines: `RESULT G1 ...` (the fingerprint against the Mac's
# and the 5090's), `RESULT LADDER pack=<name> ops=<N> mhs=<x> watts=<y> uj=<z> sm_mhz=<m> ...`, a `SUMMARY {json}` line.
# Read back with `node tools/jobs.mjs <job id>`. The kit: fetch job fetch-ca3-pc1-amd-20261006 (the packs).
$ErrorActionPreference = 'Continue'
$jobName = 'pc1-4070-shadow'
$kitId = 'fetch-ca3-pc1-amd-20261006'
$started = Get-Date
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Summary([string] $status, [hashtable] $extra) {
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 4)
}
$expected = @{ 'mx8-devnet-epoch0' = '90f794dd556f7a3b'; 'sh256x13' = '59ac286fe2a5a9ef'; 'sh256x27' = '3d2e8245cc084d07'; 'sh256x53' = '4f824b15cf2b124a'; 'sh256x88' = '0572522e39a94d8a'; 'sh64x52' = '9dd010f79d8ca9f4' }
$opsPerHash = @{ 'mx8-devnet-epoch0' = 930; 'sh256x13' = 49700; 'sh256x27' = 102100; 'sh256x53' = 199600; 'sh256x88' = 330700; 'sh64x52' = 49700 }
# timed dispatches of 2^24 nonces (wall time, as the PC 2 job): a 4070-class card is expected near 40 to 50 MH/s on
# the control (approximate, no 4070 row exists yet), so 200 dispatches are about 70 to 85 s; fewer where the ALU budget binds
$batches = @{ 'mx8-devnet-epoch0' = 200; 'sh256x13' = 200; 'sh256x27' = 180; 'sh256x53' = 150; 'sh256x88' = 100; 'sh64x52' = 200 }
$order = @('mx8-devnet-epoch0', 'sh256x13', 'sh256x27', 'sh256x53', 'sh256x88', 'sh64x52', 'mx8-devnet-epoch0')
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION"
$jobs = Split-Path $env:IGNEUM_JOB_DIR
$kit = Join-Path $jobs $kitId
if (-not (Test-Path $kit)) { "RESULT error kit missing at $kit (the fetch job $kitId runs first; republish it after any app update)"; Summary 'failed' @{ error = 'kit missing' }; exit 2 }
$packs = Join-Path $kit 'packs'
if (-not (Test-Path $packs)) { "RESULT error packs missing at $packs"; Summary 'failed' @{ error = 'packs missing' }; exit 2 }
$job = $env:IGNEUM_JOB_DIR; if (-not $job) { $job = Join-Path $env:TEMP 'igneum-ca3-pc1-4070' }; New-Item -ItemType Directory -Force -Path $job | Out-Null
$inst = @("$env:LOCALAPPDATA\Programs\Igneum Miner", "$env:ProgramFiles\Igneum Miner") | Where-Object { Test-Path (Join-Path $_ 'igneum-worker-cuda.exe') } | Select-Object -First 1
if (-not $inst) { "RESULT error no installed igneum-worker-cuda.exe"; Summary 'failed' @{ error = 'no cuda worker' }; exit 2 }
$exe = Join-Path $inst 'igneum-worker-cuda.exe'
"RESULT worker $exe sha256 $((Get-FileHash -Algorithm SHA256 $exe).Hash.ToLower()) bytes $((Get-Item $exe).Length) nvrtc_dlls $((Get-ChildItem $inst -Filter 'nvrtc*.dll').Count)"
$appDir = if ($env:IGNEUM_APP_DIR) { $env:IGNEUM_APP_DIR } else { Join-Path $env:LOCALAPPDATA 'igneum\app' }
$urlFile = Join-Path $appDir 'app.url'
$base = $null
if (Test-Path $urlFile) { $base = (Get-Content -LiteralPath $urlFile -Raw).Trim().TrimEnd('/') }
# ---- the 4070 by name: nvidia-smi index (= the app's --device for the CUDA worker, detect.rs) ----
function Smi([string] $q, $idx) { try { $a = @('--query-gpu=' + $q, '--format=csv,noheader,nounits'); if ($null -ne $idx) { $a += @('-i', "$idx") }; (& nvidia-smi @a 2>$null | ForEach-Object { "$_" }) -join ' | ' } catch { 'nvidia-smi failed' } }
$gpus = @(& nvidia-smi --query-gpu=index,name,pci.bus_id,memory.total --format=csv,noheader 2>$null | ForEach-Object { "$_" })
$gpus | ForEach-Object { "RESULT gpu $_" }
$idx = $null; $gpuName = ''
foreach ($g in $gpus) { if ($g -match '^\s*(\d+),\s*([^,]*4070[^,]*),') { $idx = [int]$Matches[1]; $gpuName = $Matches[2].Trim(); break } }
if ($null -eq $idx) { "RESULT error no RTX 4070 in nvidia-smi's list"; Summary 'failed' @{ error = 'no 4070' }; exit 2 }
"RESULT device nvidia-smi index $idx name '$gpuName' limits $(Smi 'power.limit,power.min_limit,power.max_limit,clocks.max.sm' $idx)"
# ---- the card's keys from settings.json (READ ONLY), every form ----
$entries = @()
try {
$sj = Get-Content -LiteralPath (Join-Path $appDir 'settings.json') -Raw | ConvertFrom-Json
foreach ($prop in $sj.cards.PSObject.Properties) {
if ($prop.Name -like 'nvidia:*' -and $prop.Name -match '4070') {
$parts = $prop.Name -split ':'
$forms = @()
if ($parts.Count -ge 3) { $forms += ($parts[0] + ':' + $parts[1]); $forms += ($parts[0] + ':' + (($parts | Select-Object -Skip 2) -join ':')) }
$ident = 1; if ($prop.Value.identities) { $ident = [int] $prop.Value.identities }
$pp = 0; if ($prop.Value.power_pct) { $pp = [int] $prop.Value.power_pct }
$entries += [pscustomobject]@{ key = $prop.Name; forms = $forms; enabled = [bool] $prop.Value.enabled; identities = $ident; power_pct = $pp }
"RESULT card settings_key=$($prop.Name) enabled=$([bool] $prop.Value.enabled) identities=$ident power_pct=$pp derived_forms=[$($forms -join ' | ')]"
}
}
} catch { "RESULT settings_read_failed $($_.Exception.Message)" }
if ($entries.Count -eq 0) { "RESULT card none in settings.json matching nvidia:*4070*: no switch; the rows below carry whatever load the card has" }
# off: every form of every entry; restore: the derived forms first, then each key as written with ITS OWN settings, so an
# entry's own identities and cap win on the live key (the class found in job 1's first run: one entry's identities posted to every form)
function PostCards([bool] $enabled) {
if (-not $base) { "RESULT cards no app.url: nothing posted"; return }
foreach ($pass in @('derived', 'written')) {
foreach ($e in $entries) {
$keys = if ($pass -eq 'derived') { $e.forms } else { @($e.key) }
$on = if ($enabled) { $e.enabled } else { $false }
foreach ($k in $keys) {
$c = @{ key = $k; enabled = $on; identities = $e.identities }
if ($e.power_pct -gt 0) { $c.power_pct = $e.power_pct }
$body = @{ cards = @($c) } | ConvertTo-Json -Depth 5
try { Invoke-RestMethod -Uri "$base/api/cards" -Method POST -Body $body -ContentType 'application/json' -TimeoutSec 10 | Out-Null; "RESULT cards key=$k enabled=$on identities=$($e.identities) posted (from entry $($e.key))" } catch { "RESULT cards key=$k enabled=$on error=$($_.Exception.Message)" }
}
}
}
}
function Workers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" }) }
function CardWorkers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match "--device\s+$idx(\s|$)" } | ForEach-Object { $_.ProcessId }) }
function CudaApps { @(& nvidia-smi -i $idx --query-compute-apps=pid,process_name,used_memory --format=csv,noheader 2>$null | ForEach-Object { "$_" } | Where-Object { $_ -match 'igneum|sp1|prove' }) }
"RESULT workers_before $(Stamp) $((Workers) -join ' ') cuda_apps_on_$idx=[$((CudaApps) -join '; ')]"
$samples = Join-Path $job 'smi-samples.csv'
$sampler = $null
$samplerOk = $false
$results = @()
$cardAlone = $false
$switched = $false
try {
if ($entries.Count -gt 0 -and $base) {
$before = @(CardWorkers)
if ($before.Count -eq 0 -and (@(CudaApps)).Count -eq 0) { "RESULT card already quiet before the switch (no cuda worker with --device $idx, no compute app on index $idx)"; $cardAlone = $true }
else {
PostCards $false; $switched = $true
$t = 0
while ($t -lt 150) { Start-Sleep -Seconds 5; $t += 5; if ((@(CardWorkers)).Count -eq 0 -and (@(CudaApps)).Count -eq 0) { break } }
$left = @(CardWorkers); $apps = @(CudaApps)
if ($left.Count -eq 0 -and $apps.Count -eq 0) { "RESULT card $(Stamp) quiet after $t s: no cuda worker with --device $idx and no igneum compute app on index $idx (the card to itself)"; $cardAlone = $true }
else { "RESULT card $(Stamp) UNCONFIRMED after $t s: worker pids [$($left -join ' ')] compute apps [$($apps -join '; ')]; the rows below are LOADED-card figures" }
Start-Sleep -Seconds 5
}
} else { "RESULT card not switched (no entry or no app.url): the rows below carry whatever load the card has" }
"RESULT workers_during $(Stamp) $((Workers) -join ' ')"
"RESULT gpu_before $(Stamp) $(Smi 'power.draw,power.limit,clocks.sm,clocks.mem,temperature.gpu,memory.used' $idx)"
# the sampler: nvidia-smi on this index every second into a file (its own timestamps), ended in finally
$smiExe = (Get-Command nvidia-smi -ErrorAction SilentlyContinue).Source
if (-not $smiExe) { $smiExe = Join-Path $env:SystemRoot 'System32\nvidia-smi.exe' }
Remove-Item -LiteralPath $samples -Force -ErrorAction SilentlyContinue
$sampler = Start-Process -FilePath $smiExe -ArgumentList @('-i', "$idx", '--query-gpu=timestamp,power.draw,utilization.gpu,clocks.sm,clocks.mem,temperature.gpu,memory.used', '--format=csv,noheader,nounits', '-l', '1') -RedirectStandardOutput $samples -NoNewWindow -PassThru
Start-Sleep -Seconds 12
# the sampler PROVES itself in the idle window: the first three raw nvidia-smi lines, and a line with a power number, or no benches
$raw = @(); if (Test-Path -LiteralPath $samples) { $raw = @(Get-Content -LiteralPath $samples) }
$raw | Select-Object -First 3 | ForEach-Object { "RESULT sampler_raw $_" }
$nPower = @($raw | Where-Object { $_ -match '^\d{4}/\d\d/\d\d \d\d:\d\d:\d\d\.\d+,\s*[\d.]+,' }).Count
"RESULT sampler_idle lines=$($raw.Count) lines_with_power=$nPower sampler_alive=$(-not $sampler.HasExited) now: $(Smi 'power.draw,utilization.gpu,clocks.sm' $idx)"
$samplerOk = $nPower -gt 0
if (-not $samplerOk) {
$why = if ($raw.Count -eq 0) { 'nvidia-smi -l 1 wrote nothing in 12 s' } else { 'nvidia-smi wrote ' + $raw.Count + ' line(s) but none carries a timestamp and a power number (first lines above)' }
"RESULT watts error=$why; the ladder is NOT run blind"
foreach ($pk in $order) { "RESULT LADDER pack=$pk error=$why" }
} else { "RESULT idle $(Stamp) sampler proven on the idle window" }
$i = 0
foreach ($pk in $order) {
if (-not $samplerOk) { break }
$i++
$d = Join-Path $packs $pk
if (-not (Test-Path $d)) { "RESULT G1 pack=$pk error=pack missing at $d"; "RESULT LADDER pack=$pk error=pack missing"; continue }
$instrs = 0; $cls = ''
try { $pj = Get-Content -LiteralPath (Join-Path $d 'program.json') -Raw | ConvertFrom-Json; $cls = [string] $pj.load_class; if ($pj.shadow) { $instrs = [int] $pj.shadow.instrs_per_hash } } catch { }
$n = $batches[$pk]
$tag = "$pk#$i"
$t0 = Get-Date
"RESULT bench $tag start $(Stamp) batches=$n class=$cls shadow_instrs_per_hash=$instrs"
$lines = @(& $exe --bench --pack $d --batches $n --batch-log2 24 --block-warps 1 --device $idx 2>&1 | ForEach-Object { "$_" })
$rc = $LASTEXITCODE
$t1 = Get-Date
$lines | ForEach-Object { "RESULT bench $tag $_" }
"RESULT bench $tag end $(Stamp) exit=$rc wall_s=$([int]($t1 - $t0).TotalSeconds)"
$res = $lines | Where-Object { $_ -match '^RESULT pack=' } | Select-Object -First 1
$fp = ''; $mhs = ''; $check = ''; $devName = ''; $nvrtc = ''; $ds = ''
if ($res -and $res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] }
if ($res -and $res -match 'mhs=([\d.]+)') { $mhs = $Matches[1] }
if ($res -and $res -match 'check=(\S+)') { $check = $Matches[1] }
if ($res -and $res -match 'device=(\S+)') { $devName = $Matches[1] }
$tl = $lines | Where-Object { $_ -match 'nvrtc (\d+) cache \d+ dataset (\d+)' } | Select-Object -First 1
if ($tl -and $tl -match 'nvrtc (\d+) cache \d+ dataset (\d+)') { $nvrtc = $Matches[1]; $ds = $Matches[2] }
if (-not $res -or $rc -ne 0) {
$err = ($lines | Where-Object { $_ -match 'FAIL|error|MISMATCH' } | Select-Object -First 1); if (-not $err) { $err = "exit $rc, no RESULT line" }
"RESULT G1 pack=$pk error=$($err -replace '\s+', ' ')"; "RESULT LADDER pack=$pk error=$($err -replace '\s+', ' ')"; continue
}
if ($devName -notmatch '4070') { "RESULT G1 pack=$pk error=the worker ran on device '$devName', not the 4070 (CUDA ordinal $idx is another card: the row is not taken)"; "RESULT LADDER pack=$pk error=wrong device $devName"; continue }
$want = $expected[$pk]
$match = if ($fp -eq $want) { 'yes' } else { 'no' }
"RESULT G1 pack=$pk fingerprint=$fp match=$match expected=$want check=$check card=RTX4070 harness=cuda-installed"
$results += [pscustomobject]@{ tag = $tag; pack = $pk; instrs = $instrs; ops = $opsPerHash[$pk]; mhs = $mhs; fp = $fp; match = $match; check = $check; nvrtc_ms = $nvrtc; dataset_ms = $ds; dev = $devName; t0 = $t0; t1 = $t1 }
Start-Sleep -Seconds 3
}
} finally {
if ($sampler) { try { Stop-Process -Id $sampler.Id -Force -ErrorAction SilentlyContinue; "RESULT sampler ended pid=$($sampler.Id)" } catch { } }
if ($switched) {
PostCards $true
"RESULT card restore posted: each settings.json entry with its own enabled flag, identities and cap"
$t = 0; $back = @()
$anyEnabled = ($entries | Where-Object { $_.enabled }).Count -gt 0
while ($t -lt 90 -and $anyEnabled) { Start-Sleep -Seconds 5; $t += 5; $back = @(CardWorkers); if ($back.Count -gt 0) { break } }
"RESULT card_workers_after $(Stamp) pids=[$($back -join ' ')] after $t s $(if ($back.Count -gt 0) { '(the card mines again)' } elseif ($anyEnabled) { '(NOT back yet: check the app)' } else { '(no entry was enabled before this job)' })"
}
"RESULT workers_after $(Stamp) $((Workers) -join ' ')"
"RESULT gpu_after $(Stamp) $(Smi 'power.draw,power.limit,clocks.sm,clocks.mem,temperature.gpu,memory.used' $idx)"
}
# ---- the samples: the mean over each bench window after its first 12 s (NVRTC, fills, warm-up) and before its last 2 s ----
$rows = @()
if (Test-Path $samples) {
foreach ($l in (Get-Content -LiteralPath $samples)) {
$p = $l -split ',\s*'
if ($p.Count -lt 7) { continue }
try { $ts = [datetime]::ParseExact($p[0].Trim(), 'yyyy/MM/dd HH:mm:ss.fff', $null) } catch { continue }
try { $rows += [pscustomobject]@{ ts = $ts; w = [double]$p[1]; util = [double]$p[2]; sm = [double]$p[3]; mem = [double]$p[4]; temp = [double]$p[5]; used = [double]$p[6] } } catch { }
}
}
"RESULT samples total $($rows.Count) file $samples"
$g1Match = 0
foreach ($r in $results) {
$in = @($rows | Where-Object { $_.ts -gt $r.t0.AddSeconds(12) -and $_.ts -lt $r.t1.AddSeconds(-2) })
$watts = 'owed'; $uj = 'owed'; $sm = 'owed'; $mem = 'owed'; $tmax = 'owed'; $util = 'owed'; $wmin = ''; $wmax = ''
if ($in.Count -gt 0) {
$m = $in | Measure-Object -Property w -Average -Minimum -Maximum
$watts = [math]::Round($m.Average, 1); $wmin = $m.Minimum; $wmax = $m.Maximum
$sm = [math]::Round(($in | Measure-Object -Property sm -Average).Average); $mem = [math]::Round(($in | Measure-Object -Property mem -Average).Average)
$util = [math]::Round(($in | Measure-Object -Property util -Average).Average, 1); $tmax = ($in | Measure-Object -Property temp -Maximum).Maximum
if ($r.mhs -and [double] $r.mhs -gt 0) { $uj = [math]::Round($m.Average / [double] $r.mhs, 3) }
}
if ($r.match -eq 'yes') { $g1Match++ }
"RESULT LADDER pack=$($r.pack) ops=$($r.ops) shadow_instrs=$($r.instrs) mhs=$($r.mhs) watts=$watts uj=$uj watts_min=$wmin watts_max=$wmax sm_mhz=$sm mem_mhz=$mem util=$util temp_max=$tmax samples=$($in.Count) window_s=$([int]($r.t1 - $r.t0).TotalSeconds) nvrtc_ms=$($r.nvrtc_ms) dataset_ms=$($r.dataset_ms) fingerprint=$($r.fp) match=$($r.match) card_alone=$cardAlone device=$($r.dev) tag=$($r.tag)"
}
foreach ($r in $rows) { "RESULT sample $($r.ts.ToString('HH:mm:ss')) $($r.w) $($r.util) $($r.sm) $($r.mem) $($r.temp) $($r.used)" }
$status = if (-not $samplerOk) { 'failed' } elseif ($results.Count -eq $order.Count) { 'done' } elseif ($results.Count -gt 0) { 'partial' } else { 'failed' }
"RESULT end $(Stamp)"
Summary $status @{ packs_run = $results.Count; packs_wanted = $order.Count; g1_match = $g1Match; sampler_proven = $samplerOk; card_alone = $cardAlone; smi_index = $idx; device = $gpuName }
if ($status -eq 'failed') { exit 1 }
exit 0

View file

@ -0,0 +1,108 @@
# Counter ASIC 3.0, PC 1 AMD job 3 (6 October 2026): item 2's per-day item-derivation packs (dr736-genesis and
# dr736-devnet-epoch0, docs/plans/counter-asic-3-derivation.md) on PC 1's RX 9070 XT (machine ae432dc7, gfx1201)
# through the kit's igneum-worker-opencl.exe, with the class v3 control mx8-devnet-epoch0 in the same job. Published
# as a plain signed `run` job (NOT --stop-miners) and run BESIDE THE MINERS: the rows this job is for are build and
# compile times and bit-exactness, which a shared card does not change, plus a 60 s rate whose ratio to the control
# in the same job stands on a shared card (absolutes do not; the process list says what the card carried). Nothing
# is switched; the script never quits, pauses, resumes or updates the installed app and never writes settings.json.
# Per pack, two passes: pass 1 is the cold compile (the first clBuildProgram of that kernel text on this PC today;
# AMD's driver caches compiled kernels on disk, so pass 2 is the cached figure and is printed beside it), the 1 GiB
# daily build (the worker's `cache .. dataset .. ms` line), the self-test (cache FNV, dataset head, 64 samples, 96
# vector lanes) and the 2^24 fingerprint at base nonce 0 against the Mac's (dr736-genesis 50e3eaa779da4f1e,
# dr736-devnet-epoch0 9553f6d5c667205a, mx8-devnet-epoch0 90f794dd556f7a3b) with 60 timed dispatches for the rate;
# pass 2 takes 3 dispatches. The compile time is the worker's own `build <ms> clBuildProgram` line (host.c of branch
# ca3-pc1-amd), the OpenCL equivalent of the CUDA worker's NVRTC line (+1.1 s per dr736 pack on the 5090).
# Lines: `RESULT DERIVE pack=<name> build_ms=<x> build_ms_cached=<x2> dataset_ms=<y> fingerprint=<hex> match=<yes/no>
# mhs=<z> ratio_to_mx8=<r> ...`; a `SUMMARY {json}` line at the end. Read back with `node tools/jobs.mjs <job id>`.
$ErrorActionPreference = 'Continue'
$jobName = 'pc1-amd-derive'
$kitId = 'fetch-ca3-pc1-amd-20261006'
$started = Get-Date
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Summary([string] $status, [hashtable] $extra) {
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 4)
}
$expected = @{ 'mx8-devnet-epoch0' = '90f794dd556f7a3b'; 'dr736-genesis' = '50e3eaa779da4f1e'; 'dr736-devnet-epoch0' = '9553f6d5c667205a' }
$order = @('mx8-devnet-epoch0', 'dr736-genesis', 'dr736-devnet-epoch0')
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION"
$jobs = Split-Path $env:IGNEUM_JOB_DIR
$kit = Join-Path $jobs $kitId
if (-not (Test-Path $kit)) { "RESULT error kit missing at $kit (the fetch job $kitId runs first; republish it after any app update)"; Summary 'failed' @{ error = 'kit missing' }; exit 2 }
$exe = Join-Path $kit 'bin\igneum-worker-opencl.exe'
$packs = Join-Path $kit 'packs'
if (-not (Test-Path $exe)) { "RESULT error worker missing at $exe"; Summary 'failed' @{ error = 'worker missing' }; exit 2 }
if (-not (Test-Path $packs)) { "RESULT error packs missing at $packs"; Summary 'failed' @{ error = 'packs missing' }; exit 2 }
"RESULT worker kit $exe sha256 $((Get-FileHash -Algorithm SHA256 $exe).Hash.ToLower()) bytes $((Get-Item $exe).Length)"
foreach ($pk in $order) {
$d = Join-Path $packs $pk
if (-not (Test-Path (Join-Path $d 'kernel_bound.cl'))) { "RESULT error pack $pk missing at $d"; Summary 'failed' @{ error = "pack $pk missing" }; exit 2 }
"RESULT pack $pk kernel_bound.cl sha256 $((Get-FileHash -Algorithm SHA256 (Join-Path $d 'kernel_bound.cl')).Hash.ToLower()) program.json sha256 $((Get-FileHash -Algorithm SHA256 (Join-Path $d 'program.json')).Hash.ToLower())"
}
# the OpenCL device index of the 9070 XT (the installed worker's list when present: the app's own indices)
$inst = @("$env:LOCALAPPDATA\Programs\Igneum Miner", "$env:ProgramFiles\Igneum Miner") | Where-Object { Test-Path (Join-Path $_ 'igneum-app.exe') } | Select-Object -First 1
$listExe = $exe
if ($inst -and (Test-Path (Join-Path $inst 'igneum-worker-opencl.exe'))) { $listExe = Join-Path $inst 'igneum-worker-opencl.exe' }
$list = @(& $listExe --list 2>&1 | ForEach-Object { "$_" })
$list | ForEach-Object { "RESULT list $_" }
$dev = $null
foreach ($l in $list) { if ($l -match '^\s*\[(\d+)\].*gfx1201' -and $l -notmatch 'dup') { $dev = [int]$Matches[1]; break } }
if ($null -eq $dev) { "RESULT error no gfx1201 device in --list (the eGPU is off the bus: every row OWED)"; Summary 'failed' @{ error = 'no gfx1201' }; exit 2 }
"RESULT device $dev gfx1201 (list from $listExe)"
function Workers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" }) }
$w = @(Workers)
$loaded = ($w | Where-Object { $_ -match "igneum-worker-opencl.*--device\s+$dev(\s|$)" }).Count -gt 0
"RESULT workers_before $(Stamp) $($w -join ' ')"
"RESULT context card_state=$(if ($loaded) { 'LOADED (the card mines beside this job: build, compile and bit-exact rows stand, the rate is a ratio to the control only)' } else { 'quiet (no opencl worker with --device ' + $dev + ')' })"
function RunPack([string] $pk, [int] $batches, [string] $tag) {
$d = Join-Path $packs $pk
$t0 = Get-Date
"RESULT bench $tag start $(Stamp) batches=$batches"
$lines = @(& $exe --bench-pack --pack $d --batches $batches --batch-log2 24 --device $dev 2>&1 | ForEach-Object { "$_" })
$rc = $LASTEXITCODE
$lines | ForEach-Object { "RESULT bench $tag $_" }
"RESULT bench $tag end $(Stamp) exit=$rc wall_s=$([int]((Get-Date) - $t0).TotalSeconds)"
$o = [ordered]@{ pack = $pk; rc = $rc; build_ms = ''; cache_ms = ''; dataset_ms = ''; check_ms = ''; all_ms = ''; check = ''; fp = ''; mhs = ''; error = '' }
foreach ($l in $lines) {
if ($l -match '^build ([\d.]+) ms clBuildProgram') { $o.build_ms = $Matches[1] }
if ($l -match '^pack .*: cache (\d+) dataset (\d+) hot \d+ check (\d+) ms \((\d+) ms in all\)') { $o.cache_ms = $Matches[1]; $o.dataset_ms = $Matches[2]; $o.check_ms = $Matches[3]; $o.all_ms = $Matches[4] }
if ($l -match '^RESULT pack=') { if ($l -match 'fingerprint=([0-9a-f]{16})') { $o.fp = $Matches[1] }; if ($l -match 'mhs=([\d.]+)') { $o.mhs = $Matches[1] }; if ($l -match 'check=(\S+)') { $o.check = $Matches[1] } }
}
if (-not $o.fp -or $rc -ne 0) { $e = ($lines | Where-Object { $_ -match 'FAIL|error|MISMATCH' } | Select-Object -First 1); if (-not $e) { $e = "exit $rc, no RESULT line" }; $o.error = ($e -replace '\s+', ' ') }
return $o
}
$rows = @{}
$ok = 0
foreach ($pk in $order) {
$p1 = RunPack $pk 60 "$pk#1"
Start-Sleep -Seconds 2
$p2 = RunPack $pk 3 "$pk#2"
$rows[$pk] = @{ p1 = $p1; p2 = $p2 }
if ($p1.error) { "RESULT DERIVE pack=$pk error=$($p1.error)" } else { $ok++ }
Start-Sleep -Seconds 2
}
# the rows: build (cold and cached), dataset, fingerprint, rate and its ratio to the control measured in the same job
$ctrl = $rows['mx8-devnet-epoch0'].p1
foreach ($pk in $order) {
$p1 = $rows[$pk].p1; $p2 = $rows[$pk].p2
if ($p1.error) { continue }
$want = $expected[$pk]
$match = if ($p1.fp -eq $want) { 'yes' } else { 'no' }
$match2 = if ($p2.fp -eq $want) { 'yes' } else { 'no' }
$ratio = ''
if ($ctrl -and -not $ctrl.error -and $ctrl.mhs -and $p1.mhs -and [double] $ctrl.mhs -gt 0) { $ratio = [math]::Round([double] $p1.mhs / [double] $ctrl.mhs, 3) }
$buildDelta = ''
if ($ctrl -and -not $ctrl.error -and $ctrl.build_ms -and $p1.build_ms) { $buildDelta = [math]::Round([double] $p1.build_ms - [double] $ctrl.build_ms, 1) }
"RESULT DERIVE pack=$pk build_ms=$($p1.build_ms) build_ms_cached=$($p2.build_ms) build_ms_over_mx8=$buildDelta dataset_ms=$($p1.dataset_ms) dataset_ms_pass2=$($p2.dataset_ms) cache_ms=$($p1.cache_ms) check_ms=$($p1.check_ms) fingerprint=$($p1.fp) match=$match fingerprint_pass2=$($p2.fp) match_pass2=$match2 check=$($p1.check) mhs=$($p1.mhs) ratio_to_mx8=$ratio card_state=$(if ($loaded) { 'loaded' } else { 'quiet' }) card=gfx1201 harness=opencl-kit expected=$want"
}
"RESULT workers_after $(Stamp) $((Workers) -join ' ')"
"RESULT end $(Stamp)"
$status = if ($ok -eq $order.Count) { 'done' } elseif ($ok -gt 0) { 'partial' } else { 'failed' }
Summary $status @{ packs_ok = $ok; packs_wanted = $order.Count; device = $dev; card_state = $(if ($loaded) { 'loaded' } else { 'quiet' }) }
if ($status -eq 'failed') { exit 1 }
exit 0

View file

@ -0,0 +1,79 @@
# Counter ASIC 3.0, PC 1 AMD job 2 (6 October 2026): item 6's step cost of every reserve candidate family of spec
# 1.13.2 on PC 1's RX 9070 XT (machine ae432dc7, gfx1201), docs/plans/counter-asic-3-reserve.md, status file section
# 3 "Item 6" (the OWED AMD column). Published as a plain signed `run` job (NOT --stop-miners) and run BESIDE THE MINERS:
# a dependent-chain ratio (each family against the add-xor-rotate chain, both measured in the same minute on the
# same card) survives a shared card, so no card is switched here; the process list says what the card carried and
# every line says loaded or quiet. The probe is the kit's family-probe-cl.exe (proto-opencl/family-probe.c of branch
# ca3-pc1-amd, cross-compiled with mingw, OpenCL.dll loaded at run time; the Mac ran the same source bit-exact on
# Apple OpenCL on 6 October 2026, 15:2x UTC), run three times on every AMD OpenCL device the machine lists (the
# gfx1201 on the current platform, its older-platform duplicate, the gfx1036 when present), each run best of 3 with
# a fresh seed per repetition, device event time, bit-exact against the CPU reference on two whole 32-lane groups.
# Every family is tried through every form AMD's OpenCL C offers (clang builtins ds_bpermute / ds_swizzle / sudot4 /
# wmma, cl_khr_subgroup_shuffle, cl_intel_subgroups, cl_amd_media_ops2 amd_bfe / amd_perm, the plain C form, the
# __local emulation); a form that does not compile prints build=failed with the first line of the build log and
# the run goes on. The script never quits, pauses, resumes or updates the installed app and never writes settings.json.
# Lines: the probe's own `RESULT FAMILY name=<f> ms=<best> gsteps=<x> ratio=<r> exact=<yes/no/unverified>
# path=<native|sequence|emulated> variant=<v> ...` and `RESULT FAMILYBEST ...` (the row per family the status file
# takes), each prefixed `RESULT run=<n> dev=<d>`; a `SUMMARY {json}` line at the end. Read back with `node tools/jobs.mjs <job id>`.
$ErrorActionPreference = 'Continue'
$jobName = 'pc1-amd-family'
$kitId = 'fetch-ca3-pc1-amd-20261006'
$started = Get-Date
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Summary([string] $status, [hashtable] $extra) {
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 4)
}
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION"
$jobs = Split-Path $env:IGNEUM_JOB_DIR
$kit = Join-Path $jobs $kitId
if (-not (Test-Path $kit)) { "RESULT error kit missing at $kit (the fetch job $kitId runs first; republish it after any app update)"; Summary 'failed' @{ error = 'kit missing' }; exit 2 }
$probe = Join-Path $kit 'bin\family-probe-cl.exe'
$src = Join-Path $kit 'src\family-probe.c'
if (-not (Test-Path $probe)) { "RESULT error probe missing at $probe"; Summary 'failed' @{ error = 'probe missing' }; exit 2 }
"RESULT probe $probe sha256 $((Get-FileHash -Algorithm SHA256 $probe).Hash.ToLower()) bytes $((Get-Item $probe).Length)"
if (Test-Path $src) { "RESULT source family-probe.c sha256 $((Get-FileHash -Algorithm SHA256 $src).Hash.ToLower()) bytes $((Get-Item $src).Length)" }
# what the card carries during the run (read only; nothing is switched)
function Workers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" }) }
$w = @(Workers)
$amdLoaded = ($w | Where-Object { $_ -match 'igneum-worker-opencl' }).Count -gt 0
"RESULT workers_before $(Stamp) $($w -join ' ')"
"RESULT context card_state=$(if ($amdLoaded) { 'LOADED (an igneum-worker-opencl process mines on an AMD card: absolutes are shared-card figures, ratios stand)' } else { 'quiet (no igneum-worker-opencl process)' })"
# every AMD device the probe lists (both platform entries of the 9070 XT, the gfx1036 when present)
$list = @(& $probe --list 2>&1 | ForEach-Object { "$_" })
$list | ForEach-Object { "RESULT list $_" }
$devs = @()
foreach ($l in $list) { if ($l -match '^\[(\d+)\] (.*?) \| (.*?) \|' -and $l -match 'gfx1201|gfx1036|AMD|Radeon') { if ($l -notmatch 'NVIDIA') { $devs += [pscustomobject]@{ idx = [int]$Matches[1]; name = $Matches[2]; platform = $Matches[3] } } } }
if ($devs.Count -eq 0) { "RESULT error no AMD OpenCL device in the probe's list (the eGPU is off the bus and no gfx1036: every row OWED)"; Summary 'failed' @{ error = 'no AMD device' } ; exit 2 }
"RESULT devices $(($devs | ForEach-Object { "[$($_.idx)] $($_.name) on $($_.platform)" }) -join ' ; ')"
# the gfx1201 on the current platform first (the row the status file takes), then the rest
$ordered = @($devs | Where-Object { $_.name -match 'gfx1201' } | Sort-Object { $_.platform } -Descending) + @($devs | Where-Object { $_.name -notmatch 'gfx1201' })
$runs = 0; $exactRows = 0; $rowsTotal = 0; $built = 0; $failedBuilds = 0
foreach ($d in $ordered) {
for ($r = 1; $r -le 3; $r++) {
"RESULT run=$r dev=$($d.idx) start $(Stamp) name=$($d.name) platform=$($d.platform) card_state=$(if ($amdLoaded) { 'loaded' } else { 'quiet' })"
$t0 = Get-Date
$lines = @(& $probe --device $d.idx --reps 3 2>&1 | ForEach-Object { "$_" })
$rc = $LASTEXITCODE
foreach ($l in $lines) {
if ($l -match '^RESULT ') { "RESULT run=$r dev=$($d.idx) $($l.Substring(7))" } else { "RESULT run=$r dev=$($d.idx) text $l" }
if ($l -match '^RESULT FAMILY name=\S+ ms=') { $rowsTotal++; if ($l -match ' exact=yes ') { $exactRows++ } }
if ($l -match '^RESULT FAMILY .*build=failed') { $failedBuilds++ }
if ($l -match '^RESULT FAMILYBEST name=\S+ ms=') { $built++ }
}
"RESULT run=$r dev=$($d.idx) end $(Stamp) exit=$rc wall_s=$([int]((Get-Date) - $t0).TotalSeconds)"
if ($rc -ne 0) { "RESULT run=$r dev=$($d.idx) error=probe exit $rc" }
$runs++
Start-Sleep -Seconds 2
}
}
"RESULT workers_after $(Stamp) $((Workers) -join ' ')"
"RESULT end $(Stamp)"
$status = if ($rowsTotal -gt 0) { 'done' } else { 'failed' }
Summary $status @{ runs = $runs; devices = $ordered.Count; family_rows = $rowsTotal; exact_rows = $exactRows; failed_builds = $failedBuilds; familybest_rows = $built; card_state = $(if ($amdLoaded) { 'loaded' } else { 'quiet' }) }
if ($status -eq 'failed') { exit 1 }
exit 0

View file

@ -0,0 +1,300 @@
# Counter ASIC 3.0, PC 1 AMD job 1 (6 October 2026): gate G1 for the class v4 candidate and item 8's AMD ladder on PC 1's
# RX 9070 XT (machine ae432dc7, gfx1201 on the eGPU), docs/plans/counter-asic-3-status.md sections 3, 5 and 7.
# Published as a plain signed `run` job (NOT --stop-miners): the RTX 5090 and the RTX 4070 keep mining; this script
# switches ONLY the 9070 XT off in the installed app through POST <app.url>api/cards with the card key read from the
# app's settings.json (never api/state) in EVERY form (the key as written, vendor:index, vendor:code; the 6 October
# finding: the index-less form switched nothing on 5 October), confirms by the process list that the card's
# igneum-worker-opencl process (the one with its --device index) is gone, runs the kit's igneum-worker-opencl.exe
# (proto-opencl/host.c of branch ca3-pc1-amd: the installed worker's source plus one `build <ms> clBuildProgram` line,
# cross-compiled with mingw as proto-cuda/nvrtc/build-windows.sh does) with --bench-pack on every pack of the kit while
# the AMD helper igneum-gpu-telemetry.exe (ADLX board watts, the installed app's own helper, run as a second copy at
# 1 Hz through a wrapper this script starts and ends by its own pid) samples the card, and restores the card in a
# finally block whatever happens. It never quits, pauses, resumes or updates the installed app, never writes
# settings.json and never touches the manifest. If --stop-miners IS used at publish time the card is already quiet
# and the switch is a no-op: the script works either way and says which it saw.
# Per pack: the self-test (cache, dataset, 64 samples, 96 vector lanes, from the worker's own line), the 2^24
# fingerprint at base nonce 0 against the Mac's and the 5090's (G1), and a 75 to 120 s rate with the card alone for
# MH/s; the mean board watts over the bench window give microjoules per hash. The control row doubles as item 1's AMD
# per-joule row. Lines: `RESULT G1 pack=<name> fingerprint=<hex> match=<yes/no> ...`,
# `RESULT LADDER pack=<name> ops=<N> mhs=<x> watts=<y|owed> uj=<z|owed> ...`, a `SUMMARY {json}` line at the end.
# Read back with `node tools/jobs.mjs <job id>`. The kit: fetch job fetch-ca3-pc1-amd-20261006 (tools/ca3-pc1-amd/README.md).
$ErrorActionPreference = 'Continue'
$jobName = 'pc1-amd-g1-shadow'
$kitId = 'fetch-ca3-pc1-amd-20261006'
$started = Get-Date
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Summary([string] $status, [hashtable] $extra) {
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 4)
}
# the expected 2^24 fingerprints at base nonce 0 (the Mac on Metal and Apple OpenCL, the 5090 on CUDA: status file
# section 3, docs/analysis/latency-shadow-2026-10-06.md sections 2 and 5, counter-asic-2-status.md 22:16 for the
# control; re-checked on Apple OpenCL through this kit's host.c on 6 October 2026, 15:2x UTC)
$expected = @{
'mx8-devnet-epoch0' = '90f794dd556f7a3b'
'sh256x13' = '59ac286fe2a5a9ef'
'sh256x27' = '3d2e8245cc084d07'
'sh256x53' = '4f824b15cf2b124a'
'sh256x88' = '0572522e39a94d8a'
'sh64x52' = '9dd010f79d8ca9f4'
}
# ops per hash as the status file counts them (x1.83 counted ops per shadow instruction + 930 for the base program);
# the shadow instruction count is read from each pack's program.json at run time and printed beside it
$opsPerHash = @{ 'mx8-devnet-epoch0' = 930; 'sh256x13' = 49700; 'sh256x27' = 102100; 'sh256x53' = 199600; 'sh256x88' = 330700; 'sh64x52' = 49700 }
# timed dispatches of 2^24 nonces per pack: about 0.9 s each at 19 MH/s, longer where the shadow binds the card
$batches = @{ 'mx8-devnet-epoch0' = 90; 'sh256x13' = 90; 'sh256x27' = 90; 'sh256x53' = 80; 'sh256x88' = 60; 'sh64x52' = 90 }
$order = @('mx8-devnet-epoch0', 'sh256x13', 'sh256x27', 'sh256x53', 'sh256x88', 'sh64x52', 'mx8-devnet-epoch0')
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION"
# ---- the kit (the wiped-jobs-folder rule: tested before use; republish the fetch after any app update) ----
$jobs = Split-Path $env:IGNEUM_JOB_DIR
$kit = Join-Path $jobs $kitId
if (-not (Test-Path $kit)) { "RESULT error kit missing at $kit (the fetch job $kitId runs first; republish it after any app update)"; Summary 'failed' @{ error = 'kit missing' }; exit 2 }
$exe = Join-Path $kit 'bin\igneum-worker-opencl.exe'
$packs = Join-Path $kit 'packs'
if (-not (Test-Path $exe)) { "RESULT error worker missing at $exe"; Summary 'failed' @{ error = 'worker missing' }; exit 2 }
if (-not (Test-Path $packs)) { "RESULT error packs missing at $packs"; Summary 'failed' @{ error = 'packs missing' }; exit 2 }
"RESULT worker kit $exe sha256 $((Get-FileHash -Algorithm SHA256 $exe).Hash.ToLower()) bytes $((Get-Item $exe).Length)"
foreach ($f in (Get-ChildItem $kit -Recurse -File | Where-Object { $_.Name -match '\.(exe|c|json)$|SHA256SUMS' } | Sort-Object FullName)) { "RESULT kitfile $($f.FullName.Substring($kit.Length + 1).Replace('\', '/')) sha256 $((Get-FileHash -Algorithm SHA256 $f.FullName).Hash.ToLower()) bytes $($f.Length)" }
$job = $env:IGNEUM_JOB_DIR; if (-not $job) { $job = Join-Path $env:TEMP 'igneum-ca3-pc1-amd' }; New-Item -ItemType Directory -Force -Path $job | Out-Null
# ---- the installed app: install folder (the installed worker for the cross-check and the AMD helper), app dir, URL ----
$inst = @("$env:LOCALAPPDATA\Programs\Igneum Miner", "$env:ProgramFiles\Igneum Miner") | Where-Object { Test-Path (Join-Path $_ 'igneum-app.exe') } | Select-Object -First 1
$appDir = if ($env:IGNEUM_APP_DIR) { $env:IGNEUM_APP_DIR } else { Join-Path $env:LOCALAPPDATA 'igneum\app' }
$urlFile = Join-Path $appDir 'app.url'
$base = $null
if (Test-Path $urlFile) { $base = (Get-Content -LiteralPath $urlFile -Raw).Trim().TrimEnd('/') }
"RESULT app install=$inst app_dir=$appDir url_present=$([bool]$base)"
# ---- the OpenCL device index of the 9070 XT, from the worker's own list (the current platform; the older platform's duplicate is marked dup) ----
$listExe = $exe
if ($inst -and (Test-Path (Join-Path $inst 'igneum-worker-opencl.exe'))) { $listExe = Join-Path $inst 'igneum-worker-opencl.exe' }
$list = @(& $listExe --list 2>&1 | ForEach-Object { "$_" })
$list | ForEach-Object { "RESULT list $_" }
$dev = $null
foreach ($l in $list) { if ($l -match '^\s*\[(\d+)\].*gfx1201' -and $l -notmatch 'dup') { $dev = [int]$Matches[1]; break } }
if ($null -eq $dev) { "RESULT error no gfx1201 device in --list (the eGPU is off the bus: every row OWED)"; Summary 'failed' @{ error = 'no gfx1201' }; exit 2 }
"RESULT device $dev gfx1201 (list from $listExe)"
# ---- the card's keys from settings.json (READ ONLY), every form ----
$entries = @()
try {
$sj = Get-Content -LiteralPath (Join-Path $appDir 'settings.json') -Raw | ConvertFrom-Json
foreach ($prop in $sj.cards.PSObject.Properties) {
if ($prop.Name -like 'amd:*' -and $prop.Name -match 'gfx1201') {
$parts = $prop.Name -split ':'
$forms = @()
if ($parts.Count -ge 3) { $forms += ($parts[0] + ':' + $parts[1]); $forms += ($parts[0] + ':' + $parts[2]) }
$ident = 1; if ($prop.Value.identities) { $ident = [int] $prop.Value.identities }
$entries += [pscustomobject]@{ key = $prop.Name; forms = $forms; enabled = [bool] $prop.Value.enabled; identities = $ident }
"RESULT card settings_key=$($prop.Name) enabled=$([bool] $prop.Value.enabled) identities=$ident derived_forms=[$($forms -join ' | ')]"
}
}
} catch { "RESULT settings_read_failed $($_.Exception.Message)" }
if ($entries.Count -eq 0) { "RESULT card none in settings.json matching amd:*gfx1201*: no switch; the rows below carry whatever load the card has" }
# off: every form of every entry; restore: the derived forms first, then each key as written with ITS OWN enabled flag and
# identities, so an entry's own settings win on the live key (the first run of this job, 15:46Z, posted one entry's
# identities to every form while settings.json held three gfx1201 entries: the class fixed here)
function PostCards([bool] $enabled) {
if (-not $base) { "RESULT cards no app.url: nothing posted"; return }
foreach ($pass in @('derived', 'written')) {
foreach ($e in $entries) {
$keys = if ($pass -eq 'derived') { $e.forms } else { @($e.key) }
$on = if ($enabled) { $e.enabled } else { $false }
foreach ($k in $keys) {
$body = @{ cards = @(@{ key = $k; enabled = $on; identities = $e.identities }) } | ConvertTo-Json -Depth 5
try { Invoke-RestMethod -Uri "$base/api/cards" -Method POST -Body $body -ContentType 'application/json' -TimeoutSec 10 | Out-Null; "RESULT cards key=$k enabled=$on identities=$($e.identities) posted (from entry $($e.key))" } catch { "RESULT cards key=$k enabled=$on error=$($_.Exception.Message)" }
}
}
}
}
function Workers {
@(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe' OR Name = 'igneum-worker-metal.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" })
}
function CardWorkers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match "--device\s+$dev(\s|$)" } | ForEach-Object { $_.ProcessId }) }
"RESULT workers_before $(Stamp) $((Workers) -join ' ')"
# ---- the AMD helper: the installed app's igneum-gpu-telemetry.exe (0.3.10+), else the newest one under an amd-kit job folder ----
$tele = $null
if ($inst -and (Test-Path (Join-Path $inst 'igneum-gpu-telemetry.exe'))) { $tele = Join-Path $inst 'igneum-gpu-telemetry.exe' }
if (-not $tele) { $found = Get-ChildItem -Path (Join-Path $appDir 'jobs') -Recurse -Filter 'igneum-gpu-telemetry.exe' -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match 'amd-kit' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1; if ($found) { $tele = $found.FullName } }
$wattsSource = 'owed'
if ($tele) {
$one = @(& $tele 2>&1 | ForEach-Object { "$_" })
$one | ForEach-Object { "RESULT tele_once $_" }
$card9070 = $one | Where-Object { $_ -match '^amd \d+ .*name "[^"]*9070[^"]*" watts (\S+) .* source (\S+)' } | Select-Object -First 1
if ($card9070 -and $card9070 -match ' watts (\S+) .* source (\S+)$') { if ($Matches[1] -ne '-') { $wattsSource = $Matches[2] } }
"RESULT tele helper=$tele sha256=$((Get-FileHash -Algorithm SHA256 $tele).Hash.ToLower()) watts_source=$wattsSource (board watts through ADLX GPUPower, GPUTotalBoardPower as the fallback; 'owed' = the helper gives no watts for the 9070 XT)"
} else { "RESULT tele none (no igneum-gpu-telemetry.exe in the install folder or under an amd-kit job folder): watts OWED" }
$samples = Join-Path $job 'amd-samples.log'
$sampler = $null
$windows = @()
$results = @()
$cardAlone = $false
$switched = $false
try {
# ---- the card off, confirmed by the process list (never api/state) ----
if ($entries.Count -gt 0 -and $base) {
$before = @(CardWorkers)
"RESULT card_workers_before pids=[$($before -join ' ')]"
if ($before.Count -eq 0) { "RESULT card already quiet before the switch (no opencl worker with --device ${dev}: --stop-miners or the card is off in the app)"; $cardAlone = $true }
else {
PostCards $false; $switched = $true
$t = 0
while ($t -lt 150) { Start-Sleep -Seconds 5; $t += 5; if ((@(CardWorkers)).Count -eq 0) { break } }
$left = @(CardWorkers)
if ($left.Count -eq 0) { "RESULT card $(Stamp) quiet after $t s: no igneum-worker-opencl process with --device $dev (the card to itself)"; $cardAlone = $true }
else { "RESULT card $(Stamp) UNCONFIRMED after $t s: opencl worker pids [$($left -join ' ')] still carry --device $dev; the rows below are LOADED-card figures" }
Start-Sleep -Seconds 5
}
} else { "RESULT card not switched (no entry or no app.url): the rows below carry whatever load the card has" }
"RESULT workers_during $(Stamp) $((Workers) -join ' ')"
# ---- the sampler: the helper at 1 Hz through a wrapper; its paths through the ENVIRONMENT (inherited by the child,
# never on a command line: Start-Process -ArgumentList in Windows PowerShell 5.1 joins arguments WITHOUT quoting,
# and "Igneum Miner" holds a space, which is why the 15:46Z run wrote no samples), the -File path quoted; ended
# by its own pid in finally. The sampler PROVES itself in the 12 idle seconds: the first three raw lines are
# printed, and without a 9070 XT line with watts the ladder is NOT run blind (the rates are on record from 15:46Z). ----
$samplerOk = $false
if ($tele -and $wattsSource -ne 'owed') {
$wrapper = Join-Path $job 'sampler.ps1'
$wrapperText = @'
$exe = $env:IGNEUM_SAMPLER_EXE
$out = $env:IGNEUM_SAMPLER_OUT
Add-Content -LiteralPath $out -Value ('wrapper start ' + (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fff') + ' exe=' + $exe + ' exists=' + (Test-Path -LiteralPath $exe))
& $exe -l 1 2>&1 | ForEach-Object { Add-Content -LiteralPath $out -Value ((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fff') + ' ' + $_) }
Add-Content -LiteralPath $out -Value ('wrapper end ' + (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fff') + ' exit=' + $LASTEXITCODE)
'@
[IO.File]::WriteAllText($wrapper, $wrapperText, (New-Object System.Text.UTF8Encoding $false))
Remove-Item -LiteralPath $samples -Force -ErrorAction SilentlyContinue
$env:IGNEUM_SAMPLER_EXE = $tele
$env:IGNEUM_SAMPLER_OUT = $samples
$sampler = Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ('"' + $wrapper + '"')) -WindowStyle Hidden -PassThru
"RESULT sampler pid=$($sampler.Id) wrapper=$wrapper file=$samples exe_via_env=$tele"
Start-Sleep -Seconds 12
$raw = @(); if (Test-Path -LiteralPath $samples) { $raw = @(Get-Content -LiteralPath $samples) }
$raw | Select-Object -First 3 | ForEach-Object { "RESULT sampler_raw $_" }
$n9070 = @($raw | Where-Object { $_ -match '^\S+ amd \d+ .*name "[^"]*9070[^"]*" watts [\d.]+ ' }).Count
"RESULT sampler_idle lines=$($raw.Count) lines_9070_with_watts=$n9070 sampler_alive=$(-not $sampler.HasExited)"
if ($n9070 -eq 0) {
$why = if ($raw.Count -eq 0) { 'the sampler wrote nothing in 12 s (file absent or empty)' } else { 'the sampler wrote ' + $raw.Count + ' line(s) but none is a 9070 XT line with a watts number (first lines above)' }
"RESULT watts error=$why; the ladder is NOT run blind"
foreach ($pk in $order) { "RESULT LADDER pack=$pk error=$why" }
} else { $samplerOk = $true; "RESULT idle $(Stamp) sampler proven on the idle window" }
} else { "RESULT watts error=no helper or no watts in its one-shot sample: the ladder runs with watts owed"; $samplerOk = $true }
# ---- the ladder ----
$i = 0
foreach ($pk in $order) {
if (-not $samplerOk) { break }
$i++
$d = Join-Path $packs $pk
if (-not (Test-Path $d)) { "RESULT G1 pack=$pk error=pack missing at $d"; "RESULT LADDER pack=$pk error=pack missing"; continue }
$instrs = 0; $cls = ''
try { $pj = Get-Content -LiteralPath (Join-Path $d 'program.json') -Raw | ConvertFrom-Json; $cls = [string] $pj.load_class; if ($pj.shadow) { $instrs = [int] $pj.shadow.instrs_per_hash } } catch { "RESULT note program.json of $pk unreadable: $($_.Exception.Message)" }
$n = $batches[$pk]
$tag = "$pk#$i"
$t0 = (Get-Date).ToUniversalTime()
"RESULT bench $tag start $(Stamp) batches=$n class=$cls shadow_instrs_per_hash=$instrs"
$lines = @(& $exe --bench-pack --pack $d --batches $n --batch-log2 24 --device $dev 2>&1 | ForEach-Object { "$_" })
$rc = $LASTEXITCODE
$t1 = (Get-Date).ToUniversalTime()
$lines | ForEach-Object { "RESULT bench $tag $_" }
"RESULT bench $tag end $(Stamp) exit=$rc wall_s=$([int]($t1 - $t0).TotalSeconds)"
$res = $lines | Where-Object { $_ -match '^RESULT pack=' } | Select-Object -First 1
$fp = ''; $mhs = ''; $check = ''
if ($res -and $res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] }
if ($res -and $res -match 'mhs=([\d.]+)') { $mhs = $Matches[1] }
if ($res -and $res -match 'check=(\S+)') { $check = $Matches[1] }
$buildMs = ''; if (($lines | Where-Object { $_ -match '^build ([\d.]+) ms clBuildProgram' } | Select-Object -First 1) -match '^build ([\d.]+) ms') { $buildMs = $Matches[1] }
$dsMs = ''; if (($lines | Where-Object { $_ -match '^pack .*: cache \d+ dataset (\d+) ' } | Select-Object -First 1) -match ' dataset (\d+) ') { $dsMs = $Matches[1] }
if (-not $res -or $rc -ne 0) {
$err = ($lines | Where-Object { $_ -match 'FAIL|error|MISMATCH' } | Select-Object -First 1); if (-not $err) { $err = "exit $rc, no RESULT line" }
"RESULT G1 pack=$pk error=$($err -replace '\s+', ' ')"
"RESULT LADDER pack=$pk error=$($err -replace '\s+', ' ')"
continue
}
$want = $expected[$pk]
$match = if ($want -and $fp -eq $want) { 'yes' } elseif ($want) { 'no' } else { 'unknown' }
"RESULT G1 pack=$pk fingerprint=$fp match=$match expected=$want check=$check card=gfx1201 harness=opencl-kit"
$results += [pscustomobject]@{ tag = $tag; pack = $pk; instrs = $instrs; ops = $opsPerHash[$pk]; mhs = $mhs; fp = $fp; match = $match; check = $check; build_ms = $buildMs; dataset_ms = $dsMs; t0 = $t0; t1 = $t1 }
Start-Sleep -Seconds 3
}
# ---- the cross-check: the INSTALLED worker on the control pack, if it carries --bench-pack ----
if ($inst -and (Test-Path (Join-Path $inst 'igneum-worker-opencl.exe'))) {
$iexe = Join-Path $inst 'igneum-worker-opencl.exe'
$help = (& $iexe --help 2>&1 | Out-String)
if ($help -match '--bench-pack') {
$d = Join-Path $packs 'mx8-devnet-epoch0'
$t0 = (Get-Date).ToUniversalTime()
"RESULT xcheck start $(Stamp) worker=installed sha256=$((Get-FileHash -Algorithm SHA256 $iexe).Hash.ToLower())"
$lines = @(& $iexe --bench-pack --pack $d --batches 40 --batch-log2 24 --device $dev 2>&1 | ForEach-Object { "$_" })
$rc = $LASTEXITCODE
$t1 = (Get-Date).ToUniversalTime()
$lines | Where-Object { $_ -match '^RESULT|^pack |^warm-up|FAIL|error' } | ForEach-Object { "RESULT xcheck $_" }
$res = $lines | Where-Object { $_ -match '^RESULT pack=' } | Select-Object -First 1
$fp = ''; $mhs = ''
if ($res -and $res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] }
if ($res -and $res -match 'mhs=([\d.]+)') { $mhs = $Matches[1] }
if ($res) { "RESULT XCHECK worker=installed pack=mx8-devnet-epoch0 fingerprint=$fp match=$(if ($fp -eq $expected['mx8-devnet-epoch0']) { 'yes' } else { 'no' }) mhs=$mhs exit=$rc (the kit worker's control rows are the LADDER lines)"; $windows += [pscustomobject]@{ tag = 'xcheck-installed'; t0 = $t0; t1 = $t1 } }
else { "RESULT XCHECK worker=installed error=exit $rc, no RESULT line" }
} else { "RESULT XCHECK worker=installed skipped: the installed igneum-worker-opencl.exe has no --bench-pack" }
}
} finally {
# ---- whatever happened: the sampler ended by its own pid (never by name: the app runs its own copy of the helper), the card back ----
if ($sampler) { try { & taskkill /T /F /PID $sampler.Id 2>&1 | Out-Null; "RESULT sampler ended pid=$($sampler.Id)" } catch { "RESULT sampler end error=$($_.Exception.Message)" } }
if ($switched) {
PostCards $true
"RESULT card restore posted: each settings.json entry with its own enabled flag and identities"
$t = 0; $back = @()
$anyEnabled = ($entries | Where-Object { $_.enabled }).Count -gt 0
while ($t -lt 90 -and $anyEnabled) { Start-Sleep -Seconds 5; $t += 5; $back = @(CardWorkers); if ($back.Count -gt 0) { break } }
"RESULT card_workers_after $(Stamp) pids=[$($back -join ' ')] after $t s $(if ($back.Count -gt 0) { '(the card mines again)' } elseif ($anyEnabled) { '(NOT back yet: check the app)' } else { '(no entry was enabled before this job)' })"
}
"RESULT workers_after $(Stamp) $((Workers) -join ' ')"
}
# ---- the samples: board watts and clocks of the 9070 XT inside each bench window (after its first 12 s, before its last 2 s) ----
$rows = @()
if (Test-Path $samples) {
foreach ($l in (Get-Content -LiteralPath $samples)) {
if ($l -match '^(\S+) amd (\d+) bus (\S+) kind (\S+) name "([^"]*)" watts (\S+) temp_c (\S+) fan_rpm (\S+) fan_pct (\S+) mclk_mhz (\S+) gclk_mhz (\S+) util_pct (\S+) source (\S+)') {
if ($Matches[5] -notmatch '9070') { continue }
try { $ts = [datetime]::ParseExact($Matches[1], 'yyyy-MM-ddTHH:mm:ss.fff', [Globalization.CultureInfo]::InvariantCulture) } catch { continue }
$w = if ($Matches[6] -eq '-') { $null } else { [double] $Matches[6] }
$rows += [pscustomobject]@{ ts = $ts; w = $w; temp = $Matches[7]; fan = $Matches[9]; mclk = $Matches[10]; gclk = $Matches[11]; util = $Matches[12]; src = $Matches[13] }
}
}
}
"RESULT samples total $($rows.Count) file $samples (9070 XT lines)"
function Window([datetime] $t0, [datetime] $t1) { @($rows | Where-Object { $_.ts -gt $t0.AddSeconds(12) -and $_.ts -lt $t1.AddSeconds(-2) -and $null -ne $_.w }) }
$g1Match = 0; $ladderRows = 0
foreach ($r in $results) {
$in = @(Window $r.t0 $r.t1)
$watts = 'owed'; $uj = 'owed'; $gclk = 'owed'; $mclk = 'owed'; $tmax = 'owed'; $wmin = ''; $wmax = ''
if ($in.Count -gt 0) {
$m = $in | Measure-Object -Property w -Average -Minimum -Maximum
$watts = [math]::Round($m.Average, 1); $wmin = $m.Minimum; $wmax = $m.Maximum
$gclk = [math]::Round((($in | ForEach-Object { [double] $_.gclk }) | Measure-Object -Average).Average)
$mclk = [math]::Round((($in | ForEach-Object { [double] $_.mclk }) | Measure-Object -Average).Average)
$tmax = (($in | ForEach-Object { [double] $_.temp }) | Measure-Object -Maximum).Maximum
if ($r.mhs -and [double] $r.mhs -gt 0) { $uj = [math]::Round($m.Average / [double] $r.mhs, 3) }
}
if ($r.match -eq 'yes') { $g1Match++ }
$ladderRows++
"RESULT LADDER pack=$($r.pack) ops=$($r.ops) shadow_instrs=$($r.instrs) mhs=$($r.mhs) watts=$watts uj=$uj watts_min=$wmin watts_max=$wmax gclk_mhz=$gclk mclk_mhz=$mclk temp_max=$tmax samples=$($in.Count) window_s=$([int]($r.t1 - $r.t0).TotalSeconds) build_ms=$($r.build_ms) dataset_ms=$($r.dataset_ms) fingerprint=$($r.fp) match=$($r.match) card_alone=$cardAlone tag=$($r.tag)"
}
foreach ($win in $windows) { $in = @(Window $win.t0 $win.t1); if ($in.Count -gt 0) { $m = $in | Measure-Object -Property w -Average; "RESULT power $($win.tag) samples=$($in.Count) watts_mean=$([math]::Round($m.Average, 1))" } }
if ($rows.Count -gt 0) {
$idle = @($rows | Where-Object { $results.Count -gt 0 -and $_.ts -lt $results[0].t0 -and $null -ne $_.w })
if ($idle.Count -gt 0) { "RESULT idle_power samples=$($idle.Count) watts_mean=$([math]::Round(($idle | Measure-Object -Property w -Average).Average, 1)) (the card switched off in the app, before the first bench)" }
foreach ($r in $rows) { "RESULT sample $($r.ts.ToString('HH:mm:ss')) $($r.w) $($r.temp) $($r.fan) $($r.mclk) $($r.gclk) $($r.util) $($r.src)" }
}
$status = if (-not $samplerOk) { 'failed' } elseif ($results.Count -eq $order.Count) { 'done' } elseif ($results.Count -gt 0) { 'partial' } else { 'failed' }
"RESULT end $(Stamp)"
Summary $status @{ packs_run = $results.Count; packs_wanted = $order.Count; g1_match = $g1Match; ladder_rows = $ladderRows; watts = $wattsSource; sampler_proven = $samplerOk; card_alone = $cardAlone; device = $dev }
if ($status -eq 'failed') { exit 1 }
exit 0

View file

@ -0,0 +1,240 @@
# Counter ASIC 3.0, PC 1 AMD job 1b (6 October 2026, 16:xx UTC): the WATTS re-run for the two rows job 1 left owed.
# Job 1 (run-ca3-pc1-amd-g1-shadow-20261006, 15:46 to 15:56Z) gave G1 7 of 7 and the ladder's rates but every
# LADDER row read watts=owed with samples=0: the helper's one-shot call worked (140.0 W on the 9070 XT) and the 1 Hz
# sampler wrote nothing, because Start-Process -ArgumentList in Windows PowerShell 5.1 joins its arguments WITHOUT
# quoting, and the helper's path holds a space ("Igneum Miner"), so the wrapper received a split path and its
# `& $Exe -l 1` failed silently. Here the wrapper takes its paths from environment variables (inherited by the child,
# no quoting) and the -File path is quoted; and the sampler PROVES ITSELF in the 12 idle seconds before any bench:
# the first three raw helper lines are printed, and if no 9070 XT line with watts arrived the job prints
# `RESULT watts error=<why>` and ends WITHOUT the benches (the card restored in finally), never running blind.
# Packs: the class v3 control mx8-devnet-epoch0 and the class v4 candidate sh256x27, 90 s each, the 9070 XT ALONE
# (switched off in the installed app through POST api/cards with every key form of every settings.json entry for
# the card, confirmed quiet by the process list, each entry restored with ITS OWN settings in finally). Plain signed
# `run` job (NOT --stop-miners): the 5090 and the 4070 keep mining. Never quits, pauses, resumes or updates the app,
# never writes settings.json. Watts = ADLX board watts from the installed app's own igneum-gpu-telemetry.exe
# (a second copy at -l 1), the mean over each bench window after its first 12 s and before its last 2 s;
# microjoules per hash = watts / MH/s. Lines: `RESULT LADDER pack=<name> ops=<N> mhs=<x> watts=<y> uj=<z> ...`,
# `RESULT G1 ...` again for the two packs, a `SUMMARY {json}` line. Read back with `node tools/jobs.mjs <job id>`.
$ErrorActionPreference = 'Continue'
$jobName = 'pc1-amd-watts'
$kitId = 'fetch-ca3-pc1-amd-20261006'
$started = Get-Date
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Summary([string] $status, [hashtable] $extra) {
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 4)
}
$expected = @{ 'mx8-devnet-epoch0' = '90f794dd556f7a3b'; 'sh256x27' = '3d2e8245cc084d07' }
$opsPerHash = @{ 'mx8-devnet-epoch0' = 930; 'sh256x27' = 102100 }
# 100 dispatches of 2^24 at the 18.9 to 19.3 MH/s job 1 measured = 87 to 89 s per pack
$batches = @{ 'mx8-devnet-epoch0' = 100; 'sh256x27' = 100 }
$order = @('mx8-devnet-epoch0', 'sh256x27')
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION"
$jobs = Split-Path $env:IGNEUM_JOB_DIR
$kit = Join-Path $jobs $kitId
if (-not (Test-Path $kit)) { "RESULT error kit missing at $kit (the fetch job $kitId runs first; republish it after any app update)"; Summary 'failed' @{ error = 'kit missing' }; exit 2 }
$exe = Join-Path $kit 'bin\igneum-worker-opencl.exe'
$packs = Join-Path $kit 'packs'
if (-not (Test-Path $exe)) { "RESULT error worker missing at $exe"; Summary 'failed' @{ error = 'worker missing' }; exit 2 }
if (-not (Test-Path $packs)) { "RESULT error packs missing at $packs"; Summary 'failed' @{ error = 'packs missing' }; exit 2 }
"RESULT worker kit $exe sha256 $((Get-FileHash -Algorithm SHA256 $exe).Hash.ToLower()) bytes $((Get-Item $exe).Length)"
$job = $env:IGNEUM_JOB_DIR; if (-not $job) { $job = Join-Path $env:TEMP 'igneum-ca3-pc1-amd' }; New-Item -ItemType Directory -Force -Path $job | Out-Null
$inst = @("$env:LOCALAPPDATA\Programs\Igneum Miner", "$env:ProgramFiles\Igneum Miner") | Where-Object { Test-Path (Join-Path $_ 'igneum-app.exe') } | Select-Object -First 1
$appDir = if ($env:IGNEUM_APP_DIR) { $env:IGNEUM_APP_DIR } else { Join-Path $env:LOCALAPPDATA 'igneum\app' }
$urlFile = Join-Path $appDir 'app.url'
$base = $null
if (Test-Path $urlFile) { $base = (Get-Content -LiteralPath $urlFile -Raw).Trim().TrimEnd('/') }
"RESULT app install=$inst app_dir=$appDir url_present=$([bool]$base)"
$listExe = $exe
if ($inst -and (Test-Path (Join-Path $inst 'igneum-worker-opencl.exe'))) { $listExe = Join-Path $inst 'igneum-worker-opencl.exe' }
$list = @(& $listExe --list 2>&1 | ForEach-Object { "$_" })
$list | ForEach-Object { "RESULT list $_" }
$dev = $null
foreach ($l in $list) { if ($l -match '^\s*\[(\d+)\].*gfx1201' -and $l -notmatch 'dup') { $dev = [int]$Matches[1]; break } }
if ($null -eq $dev) { "RESULT error no gfx1201 device in --list (the eGPU is off the bus: every row OWED)"; Summary 'failed' @{ error = 'no gfx1201' }; exit 2 }
"RESULT device $dev gfx1201 (list from $listExe)"
# ---- every settings.json entry for the card (READ ONLY), each with its own settings and its key forms ----
$entries = @()
try {
$sj = Get-Content -LiteralPath (Join-Path $appDir 'settings.json') -Raw | ConvertFrom-Json
foreach ($prop in $sj.cards.PSObject.Properties) {
if ($prop.Name -like 'amd:*' -and $prop.Name -match 'gfx1201') {
$parts = $prop.Name -split ':'
$forms = @()
if ($parts.Count -ge 3) { $forms += ($parts[0] + ':' + $parts[1]); $forms += ($parts[0] + ':' + $parts[2]) }
$ident = 1; if ($prop.Value.identities) { $ident = [int] $prop.Value.identities }
$entries += [pscustomobject]@{ key = $prop.Name; forms = $forms; enabled = [bool] $prop.Value.enabled; identities = $ident }
"RESULT card settings_key=$($prop.Name) enabled=$([bool] $prop.Value.enabled) identities=$ident derived_forms=[$($forms -join ' | ')]"
}
}
} catch { "RESULT settings_read_failed $($_.Exception.Message)" }
if ($entries.Count -eq 0) { "RESULT card none in settings.json matching amd:*gfx1201*: no switch; the rows below carry whatever load the card has" }
# off: every form of every entry; restore: the derived forms first, then each key as written with ITS OWN settings, so an
# entry's own identities win on the live key (job 1 posted one entry's identities to every form: the class fixed here)
function PostCards([bool] $enabled) {
if (-not $base) { "RESULT cards no app.url: nothing posted"; return }
foreach ($pass in @('derived', 'written')) {
foreach ($e in $entries) {
$keys = if ($pass -eq 'derived') { $e.forms } else { @($e.key) }
$on = if ($enabled) { $e.enabled } else { $false }
foreach ($k in $keys) {
$body = @{ cards = @(@{ key = $k; enabled = $on; identities = $e.identities }) } | ConvertTo-Json -Depth 5
try { Invoke-RestMethod -Uri "$base/api/cards" -Method POST -Body $body -ContentType 'application/json' -TimeoutSec 10 | Out-Null; "RESULT cards key=$k enabled=$on identities=$($e.identities) posted (from entry $($e.key))" } catch { "RESULT cards key=$k enabled=$on error=$($_.Exception.Message)" }
}
}
}
}
function Workers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" }) }
function CardWorkers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match "--device\s+$dev(\s|$)" } | ForEach-Object { $_.ProcessId }) }
"RESULT workers_before $(Stamp) $((Workers) -join ' ')"
# ---- the AMD helper: one in-process sample first (the path that worked in job 1) ----
$tele = $null
if ($inst -and (Test-Path (Join-Path $inst 'igneum-gpu-telemetry.exe'))) { $tele = Join-Path $inst 'igneum-gpu-telemetry.exe' }
if (-not $tele) { $found = Get-ChildItem -Path (Join-Path $appDir 'jobs') -Recurse -Filter 'igneum-gpu-telemetry.exe' -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match 'amd-kit' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1; if ($found) { $tele = $found.FullName } }
if (-not $tele) { "RESULT watts error=no igneum-gpu-telemetry.exe in the install folder or under an amd-kit job folder"; Summary 'failed' @{ error = 'no helper' }; exit 2 }
$one = @(& $tele 2>&1 | ForEach-Object { "$_" })
$one | ForEach-Object { "RESULT tele_once $_" }
$line9070 = $one | Where-Object { $_ -match '^amd \d+ .*name "[^"]*9070[^"]*" watts (\S+) ' } | Select-Object -First 1
if (-not $line9070 -or $line9070 -notmatch ' watts ([\d.]+) ') { "RESULT watts error=the helper's one-shot sample has no 9070 XT line with a watts number"; Summary 'failed' @{ error = 'no 9070 watts in the one-shot sample' }; exit 2 }
"RESULT tele helper=$tele sha256=$((Get-FileHash -Algorithm SHA256 $tele).Hash.ToLower()) one_shot_watts=$($Matches[1]) (ADLX GPUPower, GPUTotalBoardPower as the fallback)"
$samples = Join-Path $job 'amd-samples.log'
$sampler = $null
$results = @()
$cardAlone = $false
$switched = $false
$samplerOk = $false
try {
# ---- the card off, confirmed by the process list ----
if ($entries.Count -gt 0 -and $base) {
$before = @(CardWorkers)
"RESULT card_workers_before pids=[$($before -join ' ')]"
if ($before.Count -eq 0) { "RESULT card already quiet before the switch (no opencl worker with --device ${dev}: --stop-miners or the card is off in the app)"; $cardAlone = $true }
else {
PostCards $false; $switched = $true
$t = 0
while ($t -lt 150) { Start-Sleep -Seconds 5; $t += 5; if ((@(CardWorkers)).Count -eq 0) { break } }
$left = @(CardWorkers)
if ($left.Count -eq 0) { "RESULT card $(Stamp) quiet after $t s: no igneum-worker-opencl process with --device $dev (the card to itself)"; $cardAlone = $true }
else { "RESULT card $(Stamp) UNCONFIRMED after $t s: opencl worker pids [$($left -join ' ')] still carry --device $dev; the rows below are LOADED-card figures" }
Start-Sleep -Seconds 5
}
} else { "RESULT card not switched (no entry or no app.url): the rows below carry whatever load the card has" }
"RESULT workers_during $(Stamp) $((Workers) -join ' ')"
# ---- the sampler: the helper at 1 Hz through a wrapper; paths through the ENVIRONMENT (inherited, never quoted on a
# command line), the -File path quoted; ended by its own pid in finally ----
$wrapper = Join-Path $job 'sampler.ps1'
$wrapperText = @'
$exe = $env:IGNEUM_SAMPLER_EXE
$out = $env:IGNEUM_SAMPLER_OUT
Add-Content -LiteralPath $out -Value ('wrapper start ' + (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fff') + ' exe=' + $exe + ' exists=' + (Test-Path -LiteralPath $exe))
& $exe -l 1 2>&1 | ForEach-Object { Add-Content -LiteralPath $out -Value ((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fff') + ' ' + $_) }
Add-Content -LiteralPath $out -Value ('wrapper end ' + (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fff') + ' exit=' + $LASTEXITCODE)
'@
[IO.File]::WriteAllText($wrapper, $wrapperText, (New-Object System.Text.UTF8Encoding $false))
Remove-Item -LiteralPath $samples -Force -ErrorAction SilentlyContinue
$env:IGNEUM_SAMPLER_EXE = $tele
$env:IGNEUM_SAMPLER_OUT = $samples
$sampler = Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ('"' + $wrapper + '"')) -WindowStyle Hidden -PassThru
"RESULT sampler pid=$($sampler.Id) wrapper=$wrapper file=$samples exe_via_env=$tele"
Start-Sleep -Seconds 12
# ---- the proof: the first three raw lines, and a 9070 XT line with watts inside the idle window, or no benches ----
$raw = @(); if (Test-Path -LiteralPath $samples) { $raw = @(Get-Content -LiteralPath $samples) }
$raw | Select-Object -First 3 | ForEach-Object { "RESULT sampler_raw $_" }
$n9070 = @($raw | Where-Object { $_ -match '^\S+ amd \d+ .*name "[^"]*9070[^"]*" watts [\d.]+ ' }).Count
"RESULT sampler_idle lines=$($raw.Count) lines_9070_with_watts=$n9070 sampler_alive=$(-not $sampler.HasExited)"
if ($n9070 -eq 0) {
$why = if ($raw.Count -eq 0) { 'the sampler wrote nothing in 12 s (file absent or empty)' } else { 'the sampler wrote ' + $raw.Count + ' line(s) but none is a 9070 XT line with a watts number (first lines above)' }
"RESULT watts error=$why; the benches are NOT run blind"
"RESULT LADDER pack=mx8-devnet-epoch0 error=$why"
"RESULT LADDER pack=sh256x27 error=$why"
} else {
$samplerOk = $true
"RESULT idle $(Stamp) sampler proven on the idle window"
$i = 0
foreach ($pk in $order) {
$i++
$d = Join-Path $packs $pk
if (-not (Test-Path $d)) { "RESULT G1 pack=$pk error=pack missing at $d"; "RESULT LADDER pack=$pk error=pack missing"; continue }
$instrs = 0
try { $pj = Get-Content -LiteralPath (Join-Path $d 'program.json') -Raw | ConvertFrom-Json; if ($pj.shadow) { $instrs = [int] $pj.shadow.instrs_per_hash } } catch { }
$n = $batches[$pk]
$tag = "$pk#$i"
$t0 = (Get-Date).ToUniversalTime()
"RESULT bench $tag start $(Stamp) batches=$n shadow_instrs_per_hash=$instrs"
$lines = @(& $exe --bench-pack --pack $d --batches $n --batch-log2 24 --device $dev 2>&1 | ForEach-Object { "$_" })
$rc = $LASTEXITCODE
$t1 = (Get-Date).ToUniversalTime()
$lines | ForEach-Object { "RESULT bench $tag $_" }
"RESULT bench $tag end $(Stamp) exit=$rc wall_s=$([int]($t1 - $t0).TotalSeconds)"
$res = $lines | Where-Object { $_ -match '^RESULT pack=' } | Select-Object -First 1
$fp = ''; $mhs = ''; $check = ''
if ($res -and $res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] }
if ($res -and $res -match 'mhs=([\d.]+)') { $mhs = $Matches[1] }
if ($res -and $res -match 'check=(\S+)') { $check = $Matches[1] }
if (-not $res -or $rc -ne 0) {
$err = ($lines | Where-Object { $_ -match 'FAIL|error|MISMATCH' } | Select-Object -First 1); if (-not $err) { $err = "exit $rc, no RESULT line" }
"RESULT G1 pack=$pk error=$($err -replace '\s+', ' ')"; "RESULT LADDER pack=$pk error=$($err -replace '\s+', ' ')"; continue
}
$match = if ($fp -eq $expected[$pk]) { 'yes' } else { 'no' }
"RESULT G1 pack=$pk fingerprint=$fp match=$match expected=$($expected[$pk]) check=$check card=gfx1201 harness=opencl-kit"
$results += [pscustomobject]@{ tag = $tag; pack = $pk; instrs = $instrs; ops = $opsPerHash[$pk]; mhs = $mhs; fp = $fp; match = $match; t0 = $t0; t1 = $t1 }
Start-Sleep -Seconds 3
}
}
} finally {
if ($sampler) { try { & taskkill /T /F /PID $sampler.Id 2>&1 | Out-Null; "RESULT sampler ended pid=$($sampler.Id)" } catch { "RESULT sampler end error=$($_.Exception.Message)" } }
if ($switched) {
PostCards $true
"RESULT card restore posted: each settings.json entry with its own enabled flag and identities"
$t = 0; $back = @()
$anyEnabled = ($entries | Where-Object { $_.enabled }).Count -gt 0
while ($t -lt 90 -and $anyEnabled) { Start-Sleep -Seconds 5; $t += 5; $back = @(CardWorkers); if ($back.Count -gt 0) { break } }
"RESULT card_workers_after $(Stamp) pids=[$($back -join ' ')] after $t s $(if ($back.Count -gt 0) { '(the card mines again)' } elseif ($anyEnabled) { '(NOT back yet: check the app)' } else { '(no entry was enabled before this job)' })"
}
"RESULT workers_after $(Stamp) $((Workers) -join ' ')"
}
# ---- the samples: board watts and clocks inside each bench window (after its first 12 s, before its last 2 s) ----
$rows = @()
if (Test-Path -LiteralPath $samples) {
foreach ($l in (Get-Content -LiteralPath $samples)) {
if ($l -match '^(\S+) amd (\d+) bus (\S+) kind (\S+) name "([^"]*)" watts (\S+) temp_c (\S+) fan_rpm (\S+) fan_pct (\S+) mclk_mhz (\S+) gclk_mhz (\S+) util_pct (\S+) source (\S+)') {
if ($Matches[5] -notmatch '9070') { continue }
try { $ts = [datetime]::ParseExact($Matches[1], 'yyyy-MM-ddTHH:mm:ss.fff', [Globalization.CultureInfo]::InvariantCulture) } catch { continue }
$w = if ($Matches[6] -eq '-') { $null } else { [double] $Matches[6] }
$rows += [pscustomobject]@{ ts = $ts; w = $w; temp = $Matches[7]; fan = $Matches[8]; mclk = $Matches[10]; gclk = $Matches[11]; util = $Matches[12]; src = $Matches[13] }
}
}
}
"RESULT samples total $($rows.Count) file $samples (9070 XT lines)"
function Window([datetime] $t0, [datetime] $t1) { @($rows | Where-Object { $_.ts -gt $t0.AddSeconds(12) -and $_.ts -lt $t1.AddSeconds(-2) -and $null -ne $_.w }) }
$withWatts = 0
foreach ($r in $results) {
$in = @(Window $r.t0 $r.t1)
$watts = 'owed'; $uj = 'owed'; $gclk = 'owed'; $mclk = 'owed'; $tmax = 'owed'; $wmin = ''; $wmax = ''
if ($in.Count -gt 0) {
$m = $in | Measure-Object -Property w -Average -Minimum -Maximum
$watts = [math]::Round($m.Average, 1); $wmin = $m.Minimum; $wmax = $m.Maximum
$gclk = [math]::Round((($in | ForEach-Object { [double] $_.gclk }) | Measure-Object -Average).Average)
$mclk = [math]::Round((($in | ForEach-Object { [double] $_.mclk }) | Measure-Object -Average).Average)
$tmax = (($in | ForEach-Object { [double] $_.temp }) | Measure-Object -Maximum).Maximum
if ($r.mhs -and [double] $r.mhs -gt 0) { $uj = [math]::Round($m.Average / [double] $r.mhs, 3) }
$withWatts++
}
"RESULT LADDER pack=$($r.pack) ops=$($r.ops) shadow_instrs=$($r.instrs) mhs=$($r.mhs) watts=$watts uj=$uj watts_min=$wmin watts_max=$wmax gclk_mhz=$gclk mclk_mhz=$mclk temp_max=$tmax samples=$($in.Count) window_s=$([int]($r.t1 - $r.t0).TotalSeconds) fingerprint=$($r.fp) match=$($r.match) card_alone=$cardAlone tag=$($r.tag)"
}
if ($rows.Count -gt 0) {
$idle = @($rows | Where-Object { ($results.Count -eq 0 -or $_.ts -lt $results[0].t0) -and $null -ne $_.w })
if ($idle.Count -gt 0) { "RESULT idle_power samples=$($idle.Count) watts_mean=$([math]::Round(($idle | Measure-Object -Property w -Average).Average, 1)) (the card switched off in the app, before the first bench)" }
foreach ($r in $rows) { "RESULT sample $($r.ts.ToString('HH:mm:ss')) $($r.w) $($r.temp) $($r.fan) $($r.mclk) $($r.gclk) $($r.util) $($r.src)" }
}
$status = if (-not $samplerOk) { 'failed' } elseif ($withWatts -eq $order.Count) { 'done' } elseif ($results.Count -gt 0) { 'partial' } else { 'failed' }
"RESULT end $(Stamp)"
Summary $status @{ packs_run = $results.Count; packs_wanted = $order.Count; rows_with_watts = $withWatts; sampler_proven = $samplerOk; card_alone = $cardAlone; device = $dev }
if ($status -eq 'failed') { exit 1 }
exit 0

29
tools/ci/override-json-check.sh Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# The duplicate-field class (6 October 2026): two merges each appended the proving v1 block to
# infra/fast-time/override-60x.json, so the file carried `proving_v1_activation_daa` twice; the node refuses such a file
# ("duplicate field") and the class-v3 and class-v4 fast-time gates could not start a node from it, while the fork's
# every-field test skips when the file is not beside it (it is not on the PCs). Rule: every override params file the
# harnesses and the node read parses as JSON with no duplicate key. This check fails CI when one does not.
set -euo pipefail
cd "$(dirname "$0")/../.."
fail=0
while IFS= read -r f; do
if ! python3 - "$f" <<'PY'
import json, sys, collections
def pairs(p):
c = collections.Counter(k for k, _ in p)
dups = sorted(k for k, n in c.items() if n > 1)
if dups:
raise SystemExit(f"duplicate key(s) {dups}")
return dict(p)
try:
json.load(open(sys.argv[1]), object_pairs_hook=pairs)
except SystemExit as e:
print(f"override-json: {sys.argv[1]}: {e}"); sys.exit(1)
except Exception as e:
print(f"override-json: {sys.argv[1]}: does not parse: {e}"); sys.exit(1)
PY
then fail=1; fi
done < <(git ls-files 'infra/fast-time/*.json' 'tools/finality-attacks/redteam/*.json' 'infra/**/override*.json' | sort -u)
[ "$fail" = 0 ] && echo "override-json: every override params file parses with no duplicate key"
exit $fail