Merge remote-tracking branch 'origin/master' into testnet-genesis-2
5
.github/workflows/ci.yml
vendored
|
|
@ -109,6 +109,11 @@ jobs:
|
|||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: a headless Chromium for the text-overlap sweep (Playwright outside the tree; the gate finds it through IGNEUM_PLAYWRIGHT_DIR)
|
||||
run: |
|
||||
mkdir -p /tmp/pw && cd /tmp/pw && npm init -y >/dev/null && npm i --no-audit --no-fund playwright@1.56 | tail -1
|
||||
npx playwright install --with-deps chromium | tail -1
|
||||
echo "IGNEUM_PLAYWRIGHT_DIR=/tmp/pw" >> "$GITHUB_ENV"
|
||||
- name: the tree gate, tools/ci/pre-push.sh --ci (the same script the pre-push hook runs; one line per check, a red check prints its output)
|
||||
run: bash tools/ci/pre-push.sh --ci
|
||||
- name: public stats API answers with the documented fields (the live site; master only, the endpoints exist there after the merge)
|
||||
|
|
|
|||
29
docs/design/genesis-forward-harness/digest-base-dc141409.log
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
Latency ladder inactive (no activation in the override file): every class v4 program at rung 0, 27 shadow passes; no ladder bits in the header
|
||||
Program class signal inactive (the window or the floor is absent from the override file): headers carry block version 2 exactly
|
||||
Fees on igneum-devnet-973: pgas table v0, B_p 30000000 pgas, S_p 7500000 pgas, floors 1000000000 wei per gas and 1000000000 wei per pgas; calibrated v1 from DAA score never
|
||||
Consensus params digest: 079d8a7e736abbd4b135eb1d652466ca7906ea1b200e22eac97f795e626356e7 (exchanged in the p2p handshake; a peer with another digest is refused)
|
||||
2026-10-07 15:14:29.331+02:00 [INFO ] igneumd/2.1.0-dc141409
|
||||
2026-10-07 15:14:29.331+02:00 [INFO ] Application directory: gf-digest/d-base
|
||||
2026-10-07 15:14:29.331+02:00 [INFO ] Data directory: gf-digest/d-base/igneum-devnet-973/datadir
|
||||
2026-10-07 15:14:29.331+02:00 [INFO ] Logs to console only
|
||||
2026-10-07 15:14:29.350+02:00 [INFO ] Finality v2 (igneum-devnet-973): interval 30 depth 20 window 7200 DAA dust 5 presence 20 aggregators 8 ban 7200 fold 3; rule v3 (frozen table, certificate fold) from checkpoint DAA never; 0 checkpoints known, next index 1, 0 locks, 0 keys; C1 in DAA seconds from DAA never; weight-gated deep fork choice from DAA never; leave rule (W7, delay 3600 DAA) from checkpoint DAA never, 0 leaves
|
||||
2026-10-07 15:14:29.363+02:00 [INFO ] [igneum-exec] proving v0: payouts from DAA score never, window 7200 DAA, dust 5, verifier Off
|
||||
2026-10-07 15:14:29.363+02:00 [INFO ] [igneum-exec] proving v1: segment records from DAA score never, 8 blocks a segment, unproven after 600 DAA, aggregator share 1000 bps, shard program id unknown, aggregator id unknown
|
||||
2026-10-07 15:14:29.364+02:00 [INFO ] [igneum-exec] verifying keys embedded: shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a aggregator id 0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896
|
||||
2026-10-07 15:14:29.370+02:00 [INFO ] template prewarm: on (the cached block template is rebuilt on every virtual change; IGNEUM_TEMPLATE_PREWARM=0 turns it off)
|
||||
2026-10-07 15:14:29.371+02:00 [INFO ] GRPC Server starting on: 127.0.0.1:29770
|
||||
2026-10-07 15:14:29.372+02:00 [INFO ] P2P Server starting on: 127.0.0.1:29771
|
||||
2026-10-07 15:14:29.373+02:00 [INFO ] [igneum-exec] chain follower started
|
||||
2026-10-07 15:14:29.373+02:00 [INFO ] [igneum-exec] proof verifier: Off (no SP1 verification on this node)
|
||||
2026-10-07 15:14:29.373+02:00 [INFO ] [igneum-exec] exec sync: sink edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 is chain block 0; pruning point edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 is chain block Some(0) (DAA 0); retention root edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 is chain block Some(0) (DAA 0), its body held
|
||||
2026-10-07 15:14:29.373+02:00 [INFO ] [igneum-exec] eth_ JSON-RPC listening on 127.0.0.1:26790
|
||||
2026-10-07 15:14:29.373+02:00 [INFO ] [igneum-exec] exec sync: no snapshot to resume from (no snapshot file); the follower starts at genesis
|
||||
2026-10-07 15:14:29.373+02:00 [INFO ] [igneum-exec] exec sync: waiting for consensus to sync before the executor starts (the sink is 393269 s old)
|
||||
2026-10-07 15:14:29.373+02:00 [WARN ] [igneum-exec] cannot bind the eth_ JSON-RPC server on 127.0.0.1:26790: Address already in use (os error 98)
|
||||
2026-10-07 15:14:29.374+02:00 [INFO ] WRPC Server starting on: 127.0.0.1:29772
|
||||
2026-10-07 15:14:29.474+02:00 [INFO ] [igneum-exec] chain follower stopped
|
||||
^SIGTERM - shutting down...
|
||||
2026-10-07 15:14:54.316+02:00 [INFO ] P2P Server stopped: 127.0.0.1:29771
|
||||
2026-10-07 15:14:54.316+02:00 [INFO ] WRPC Server stopped on: 127.0.0.1:29772
|
||||
2026-10-07 15:14:54.317+02:00 [INFO ] GRPC Server stopped on: 127.0.0.1:29770
|
||||
2026-10-07 15:14:54.824+02:00 [INFO ] igneumd has stopped...
|
||||
29
docs/design/genesis-forward-harness/digest-no-file.log
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
Latency ladder inactive (no activation in the override file): every class v4 program at rung 0, 27 shadow passes; no ladder bits in the header
|
||||
Program class signal inactive (the window or the floor is absent from the override file): headers carry block version 2 exactly
|
||||
Fees on igneum-devnet-973: pgas table v0, B_p 30000000 pgas, S_p 7500000 pgas, floors 1000000000 wei per gas and 1000000000 wei per pgas; calibrated v1 from DAA score never
|
||||
Consensus params digest: 079d8a7e736abbd4b135eb1d652466ca7906ea1b200e22eac97f795e626356e7 (exchanged in the p2p handshake; a peer with another digest is refused)
|
||||
2026-10-07 15:12:35.092+02:00 [INFO ] igneumd/2.1.0-75810130
|
||||
2026-10-07 15:12:35.093+02:00 [INFO ] Application directory: gf-digest/d-none
|
||||
2026-10-07 15:12:35.093+02:00 [INFO ] Data directory: gf-digest/d-none/igneum-devnet-973/datadir
|
||||
2026-10-07 15:12:35.093+02:00 [INFO ] Logs to console only
|
||||
2026-10-07 15:12:35.128+02:00 [INFO ] Finality v2 (igneum-devnet-973): interval 30 depth 20 window 7200 DAA dust 5 presence 20 aggregators 8 ban 7200 fold 3; rule v3 (frozen table, certificate fold) from checkpoint DAA never; 0 checkpoints known, next index 1, 0 locks, 0 keys; C1 in DAA seconds from DAA never; weight-gated deep fork choice from DAA never; leave rule (W7, delay 3600 DAA) from checkpoint DAA never, 0 leaves; key succession (W5) from checkpoint DAA never, 0 successions; signature scheme 0 (0 = BLS12-381), any other refused until a class names it
|
||||
2026-10-07 15:12:35.136+02:00 [INFO ] [igneum-exec] proving v0: payouts from DAA score never, window 7200 DAA, dust 5, verifier Off
|
||||
2026-10-07 15:12:35.136+02:00 [INFO ] [igneum-exec] proving v1: segment records from DAA score never, 8 blocks a segment, unproven after 600 DAA, aggregator share 1000 bps, shard program id unknown, aggregator id unknown
|
||||
2026-10-07 15:12:35.136+02:00 [INFO ] [igneum-exec] proof archive at gf-digest/d-none/igneum-devnet-973/datadir/evm/proofs: 0 proofs
|
||||
2026-10-07 15:12:35.136+02:00 [INFO ] [igneum-exec] verifying keys embedded: shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a aggregator id 0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896
|
||||
2026-10-07 15:12:35.141+02:00 [INFO ] template prewarm: on (the cached block template is rebuilt on every virtual change; IGNEUM_TEMPLATE_PREWARM=0 turns it off)
|
||||
2026-10-07 15:12:35.141+02:00 [INFO ] GRPC Server starting on: 127.0.0.1:29760
|
||||
2026-10-07 15:12:35.141+02:00 [INFO ] P2P Server starting on: 127.0.0.1:29761
|
||||
2026-10-07 15:12:35.141+02:00 [INFO ] [igneum-exec] eth_ JSON-RPC listening on 127.0.0.1:26790
|
||||
2026-10-07 15:12:35.141+02:00 [INFO ] [igneum-exec] proof verifier: Off (no SP1 verification on this node)
|
||||
2026-10-07 15:12:35.142+02:00 [INFO ] [igneum-exec] chain follower started
|
||||
2026-10-07 15:12:35.142+02:00 [INFO ] WRPC Server starting on: 127.0.0.1:29762
|
||||
2026-10-07 15:12:35.142+02:00 [WARN ] [igneum-exec] cannot bind the eth_ JSON-RPC server on 127.0.0.1:26790: Address already in use (os error 98)
|
||||
2026-10-07 15:12:35.142+02:00 [INFO ] [igneum-exec] exec sync: sink edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 is chain block 0; pruning point edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 is chain block Some(0) (DAA 0); retention root edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 is chain block Some(0) (DAA 0), its body held
|
||||
2026-10-07 15:12:35.142+02:00 [INFO ] [igneum-exec] exec sync: no snapshot to resume from (no snapshot file); the follower starts at genesis
|
||||
2026-10-07 15:12:35.142+02:00 [INFO ] [igneum-exec] chain follower stopped
|
||||
^SIGTERM - shutting down...
|
||||
2026-10-07 15:13:00.089+02:00 [INFO ] P2P Server stopped: 127.0.0.1:29761
|
||||
2026-10-07 15:13:00.089+02:00 [INFO ] GRPC Server stopped on: 127.0.0.1:29760
|
||||
2026-10-07 15:13:00.089+02:00 [INFO ] WRPC Server stopped on: 127.0.0.1:29762
|
||||
2026-10-07 15:13:00.597+02:00 [INFO ] igneumd has stopped...
|
||||
32
docs/design/genesis-forward-harness/digest-testnet-shape.log
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
Signature scheme byte from the override file: genesis scheme 0 (0 = BLS12-381); every vote item and key reveal carries its scheme byte on the wire from DAA score 0; any other scheme refused until a class the 95 percent signal moves to names it
|
||||
Key succession (W5) from the override file: a vote key hands its window weight and its forfeit term to a successor key once, from checkpoint DAA score 0; the successor inherits the window
|
||||
Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes, cache [512 MiB] beside them (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 86400 DAA ending at an epoch's seed block
|
||||
Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, cache [512 MiB] beside them, this node signals none (header version bits 15 and 14; both set asks for the cache rung)
|
||||
Program class signal inactive (the window or the floor is absent from the override file): headers carry block version 2 exactly
|
||||
Fees on igneum-devnet-973: pgas table v0, B_p 30000000 pgas, S_p 7500000 pgas, floors 1000000000 wei per gas and 1000000000 wei per pgas; calibrated v1 from DAA score never
|
||||
Consensus params digest: 60e842a738c7dea04543af93e990fb962618ace0b76b38013a63cb29dd45644a (exchanged in the p2p handshake; a peer with another digest is refused)
|
||||
2026-10-07 15:13:26.156+02:00 [INFO ] igneumd/2.1.0-75810130
|
||||
2026-10-07 15:13:26.156+02:00 [INFO ] Application directory: gf-digest/d-testnet-shape
|
||||
2026-10-07 15:13:26.156+02:00 [INFO ] Data directory: gf-digest/d-testnet-shape/igneum-devnet-973/datadir
|
||||
2026-10-07 15:13:26.156+02:00 [INFO ] Logs to console only
|
||||
2026-10-07 15:13:26.172+02:00 [INFO ] Finality v2 (igneum-devnet-973): interval 30 depth 20 window 7200 DAA dust 5 presence 20 aggregators 8 ban 7200 fold 3; rule v3 (frozen table, certificate fold) from checkpoint DAA never; 0 checkpoints known, next index 1, 0 locks, 0 keys; C1 in DAA seconds from DAA never; weight-gated deep fork choice from DAA never; leave rule (W7, delay 3600 DAA) from checkpoint DAA never, 0 leaves; key succession (W5) from checkpoint DAA 0, 0 successions; signature scheme 0 (0 = BLS12-381), any other refused until a class names it
|
||||
2026-10-07 15:13:26.180+02:00 [INFO ] [igneum-exec] proving v0: payouts from DAA score never, window 7200 DAA, dust 5, verifier Off
|
||||
2026-10-07 15:13:26.180+02:00 [INFO ] [igneum-exec] proving v1: segment records from DAA score never, 8 blocks a segment, unproven after 600 DAA, aggregator share 1000 bps, shard program id unknown, aggregator id unknown
|
||||
2026-10-07 15:13:26.180+02:00 [INFO ] [igneum-exec] proof archive at gf-digest/d-testnet-shape/igneum-devnet-973/datadir/evm/proofs: 0 proofs
|
||||
2026-10-07 15:13:26.181+02:00 [INFO ] [igneum-exec] verifying keys embedded: shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a aggregator id 0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896
|
||||
2026-10-07 15:13:26.185+02:00 [INFO ] template prewarm: on (the cached block template is rebuilt on every virtual change; IGNEUM_TEMPLATE_PREWARM=0 turns it off)
|
||||
2026-10-07 15:13:26.186+02:00 [INFO ] GRPC Server starting on: 127.0.0.1:29760
|
||||
2026-10-07 15:13:26.186+02:00 [INFO ] P2P Server starting on: 127.0.0.1:29761
|
||||
2026-10-07 15:13:26.186+02:00 [INFO ] [igneum-exec] eth_ JSON-RPC listening on 127.0.0.1:26790
|
||||
2026-10-07 15:13:26.186+02:00 [INFO ] WRPC Server starting on: 127.0.0.1:29762
|
||||
2026-10-07 15:13:26.186+02:00 [INFO ] [igneum-exec] chain follower started
|
||||
2026-10-07 15:13:26.186+02:00 [INFO ] [igneum-exec] proof verifier: Off (no SP1 verification on this node)
|
||||
2026-10-07 15:13:26.186+02:00 [WARN ] [igneum-exec] cannot bind the eth_ JSON-RPC server on 127.0.0.1:26790: Address already in use (os error 98)
|
||||
2026-10-07 15:13:26.186+02:00 [INFO ] [igneum-exec] exec sync: sink edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 is chain block 0; pruning point edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 is chain block Some(0) (DAA 0); retention root edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 is chain block Some(0) (DAA 0), its body held
|
||||
2026-10-07 15:13:26.186+02:00 [INFO ] [igneum-exec] exec sync: no snapshot to resume from (no snapshot file); the follower starts at genesis
|
||||
2026-10-07 15:13:26.186+02:00 [INFO ] [igneum-exec] chain follower stopped
|
||||
^SIGTERM - shutting down...
|
||||
2026-10-07 15:13:51.152+02:00 [INFO ] P2P Server stopped: 127.0.0.1:29761
|
||||
2026-10-07 15:13:51.152+02:00 [INFO ] WRPC Server stopped on: 127.0.0.1:29762
|
||||
2026-10-07 15:13:51.152+02:00 [INFO ] GRPC Server stopped on: 127.0.0.1:29760
|
||||
2026-10-07 15:13:51.660+02:00 [INFO ] igneumd has stopped...
|
||||
|
|
@ -0,0 +1,695 @@
|
|||
{
|
||||
"expect": "no-succession",
|
||||
"pass": false,
|
||||
"checks": {
|
||||
"window_filled_before_the_succession": true,
|
||||
"succession_accepted_on_submit": true,
|
||||
"carried_once_on_every_node": false,
|
||||
"successor_inherits_on_every_node": false,
|
||||
"old_key_handed_over_on_every_node": false,
|
||||
"nodes_agree_on_the_successor_weight": false,
|
||||
"refused_old_again_on_n2": false,
|
||||
"refused_old_again_on_n0": false,
|
||||
"refused_successor_that_handed_over": false,
|
||||
"scheme_1_refused_by_every_node": true,
|
||||
"locks_continue_after_the_fold": true,
|
||||
"zero_rejected_by_nodes": true,
|
||||
"sinks_agree": true
|
||||
},
|
||||
"old_key": "e4036b1e79c817f2",
|
||||
"new_key": "2f031ed35a29ced8",
|
||||
"old_blocks_before": 41,
|
||||
"succeeded_at_daa": 261,
|
||||
"carried_seen_at_daa": null,
|
||||
"new_mined_alone": 112,
|
||||
"rows": [
|
||||
{
|
||||
"new": {
|
||||
"blocks": 37,
|
||||
"keyHash": "2f031ed35a29ced8",
|
||||
"participation": 1,
|
||||
"pubkey": "80ab38c523736ad8944c7d3375e7563ba3b0d5a3ab149bec568cf207edcc9e1ab1ee2566e06ff422a5ec4c85a4049976",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "",
|
||||
"succeededTo": "",
|
||||
"voter": true
|
||||
},
|
||||
"old": {
|
||||
"blocks": 0,
|
||||
"keyHash": "e4036b1e79c817f2",
|
||||
"participation": 0,
|
||||
"pubkey": "b0fd8eebbbaad96268e5f7b8a86090ca0c723954a0a5cb81c92b633094c1742b2505c4f7cea646c5c96bb52e57400067",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "",
|
||||
"succeededTo": "6543d321fd61aedb",
|
||||
"voter": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"new": {
|
||||
"blocks": 0,
|
||||
"keyHash": "2f031ed35a29ced8",
|
||||
"participation": 0,
|
||||
"pubkey": "80ab38c523736ad8944c7d3375e7563ba3b0d5a3ab149bec568cf207edcc9e1ab1ee2566e06ff422a5ec4c85a4049976",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "",
|
||||
"succeededTo": "e4036b1e79c817f2",
|
||||
"voter": false
|
||||
},
|
||||
"old": {
|
||||
"blocks": 0,
|
||||
"keyHash": "e4036b1e79c817f2",
|
||||
"participation": 0,
|
||||
"pubkey": "b0fd8eebbbaad96268e5f7b8a86090ca0c723954a0a5cb81c92b633094c1742b2505c4f7cea646c5c96bb52e57400067",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "",
|
||||
"succeededTo": "6543d321fd61aedb",
|
||||
"voter": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"new": {
|
||||
"blocks": 37,
|
||||
"keyHash": "2f031ed35a29ced8",
|
||||
"participation": 1,
|
||||
"pubkey": "80ab38c523736ad8944c7d3375e7563ba3b0d5a3ab149bec568cf207edcc9e1ab1ee2566e06ff422a5ec4c85a4049976",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "",
|
||||
"succeededTo": "",
|
||||
"voter": true
|
||||
},
|
||||
"old": {
|
||||
"blocks": 0,
|
||||
"keyHash": "e4036b1e79c817f2",
|
||||
"participation": 0,
|
||||
"pubkey": "b0fd8eebbbaad96268e5f7b8a86090ca0c723954a0a5cb81c92b633094c1742b2505c4f7cea646c5c96bb52e57400067",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "",
|
||||
"succeededTo": "6543d321fd61aedb",
|
||||
"voter": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"locks": [
|
||||
12,
|
||||
12,
|
||||
12
|
||||
],
|
||||
"locks_at_fold": 9,
|
||||
"refusals": [
|
||||
{
|
||||
"what": "w5-old -> w5-third on n2",
|
||||
"code": 0,
|
||||
"line": "1791378723.612 SUCCEED old=e4036b1e79c817f2 new=6543d321fd61aedb daa=502 (accepted: carried in this node's next block)"
|
||||
},
|
||||
{
|
||||
"what": "w5-old -> w5-third on n0",
|
||||
"code": 0,
|
||||
"line": "1791378723.629 SUCCEED old=e4036b1e79c817f2 new=6543d321fd61aedb daa=502 (accepted: carried in this node's next block)"
|
||||
},
|
||||
{
|
||||
"what": "w5-new -> w5-old on n1",
|
||||
"code": 0,
|
||||
"line": "1791378723.645 SUCCEED old=2f031ed35a29ced8 new=e4036b1e79c817f2 daa=502 (accepted: carried in this node's next block)"
|
||||
}
|
||||
],
|
||||
"probes": [
|
||||
{
|
||||
"node": 0,
|
||||
"code": 0,
|
||||
"line": "1791378723.659 SCHEME 1 REFUSED key=b875b095c1b5d559 index=16 (refused: vote carries signature scheme 1; the active scheme is 0 (BLS12-381); another scheme is named only by a program class the 95 percent signal moves to, and none names one)"
|
||||
},
|
||||
{
|
||||
"node": 1,
|
||||
"code": 0,
|
||||
"line": "1791378723.672 SCHEME 1 REFUSED key=b875b095c1b5d559 index=16 (refused: vote carries signature scheme 1; the active scheme is 0 (BLS12-381); another scheme is named only by a program class the 95 percent signal moves to, and none names one)"
|
||||
},
|
||||
{
|
||||
"node": 2,
|
||||
"code": 0,
|
||||
"line": "1791378723.687 SCHEME 1 REFUSED key=b875b095c1b5d559 index=16 (refused: vote carries signature scheme 1; the active scheme is 0 (BLS12-381); another scheme is named only by a program class the 95 percent signal moves to, and none names one)"
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"t": 5.7,
|
||||
"daa": 0,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"?",
|
||||
"?",
|
||||
"?"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 20.7,
|
||||
"daa": 7,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"?",
|
||||
"?",
|
||||
"?"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 35.7,
|
||||
"daa": 17,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"?",
|
||||
"?",
|
||||
"?"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 50.7,
|
||||
"daa": 36,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"?",
|
||||
"?",
|
||||
"?"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 65.7,
|
||||
"daa": 57,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"29/3",
|
||||
"29/3",
|
||||
"29/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 80.8,
|
||||
"daa": 79,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"29/3",
|
||||
"29/3",
|
||||
"29/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 95.8,
|
||||
"daa": 92,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"59/3",
|
||||
"59/3",
|
||||
"59/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 110.8,
|
||||
"daa": 104,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"59/3",
|
||||
"59/3",
|
||||
"59/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 125.8,
|
||||
"daa": 114,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"89/3",
|
||||
"89/3",
|
||||
"89/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 140.8,
|
||||
"daa": 123,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"89/3",
|
||||
"89/3",
|
||||
"89/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 155.8,
|
||||
"daa": 135,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"89/3",
|
||||
"89/3",
|
||||
"89/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 170.9,
|
||||
"daa": 152,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"119/3",
|
||||
"119/3",
|
||||
"119/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 185.9,
|
||||
"daa": 173,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 200.9,
|
||||
"daa": 187,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 215.9,
|
||||
"daa": 203,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 230.9,
|
||||
"daa": 224,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 246,
|
||||
"daa": 242,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
3,
|
||||
3,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 261,
|
||||
"daa": 258,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
3,
|
||||
3,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 276.5,
|
||||
"daa": 270,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
4,
|
||||
4,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 291.6,
|
||||
"daa": 285,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
4,
|
||||
4,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 306.6,
|
||||
"daa": 308,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"118/3",
|
||||
"118/3",
|
||||
"118/3"
|
||||
],
|
||||
"locks": [
|
||||
4,
|
||||
4,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 321.7,
|
||||
"daa": 338,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/4",
|
||||
"120/4",
|
||||
"120/4"
|
||||
],
|
||||
"locks": [
|
||||
4,
|
||||
4,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 336.7,
|
||||
"daa": 357,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/4",
|
||||
"120/4",
|
||||
"120/4"
|
||||
],
|
||||
"locks": [
|
||||
4,
|
||||
4,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 351.8,
|
||||
"daa": 368,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/4",
|
||||
"120/4",
|
||||
"120/4"
|
||||
],
|
||||
"locks": [
|
||||
4,
|
||||
4,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 366.9,
|
||||
"daa": 385,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/4",
|
||||
"120/4",
|
||||
"120/4"
|
||||
],
|
||||
"locks": [
|
||||
5,
|
||||
5,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 382,
|
||||
"daa": 406,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/4",
|
||||
"120/4",
|
||||
"120/4"
|
||||
],
|
||||
"locks": [
|
||||
5,
|
||||
5,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 397,
|
||||
"daa": 424,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
6,
|
||||
6,
|
||||
6
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 412.1,
|
||||
"daa": 441,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
7,
|
||||
7,
|
||||
7
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 427.2,
|
||||
"daa": 454,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
7,
|
||||
7,
|
||||
7
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 442.3,
|
||||
"daa": 469,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
7,
|
||||
7,
|
||||
7
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 457.3,
|
||||
"daa": 485,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
8,
|
||||
8,
|
||||
8
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 472.4,
|
||||
"daa": 494,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
8,
|
||||
8,
|
||||
8
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 487.6,
|
||||
"daa": 509,
|
||||
"phase": "after",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
9,
|
||||
9,
|
||||
9
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 502.6,
|
||||
"daa": 526,
|
||||
"phase": "after",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
9,
|
||||
9,
|
||||
9
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 517.6,
|
||||
"daa": 545,
|
||||
"phase": "after",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
10,
|
||||
10,
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 532.7,
|
||||
"daa": 561,
|
||||
"phase": "after",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
11,
|
||||
11,
|
||||
11
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 547.7,
|
||||
"daa": 578,
|
||||
"phase": "after",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
11,
|
||||
11,
|
||||
11
|
||||
]
|
||||
}
|
||||
],
|
||||
"wall_s": 560.2,
|
||||
"binaries": {
|
||||
"igneumd": "/srv/builds/igneum-wt-genesis-forward/vendor/igneum-node-gf/target/release/igneumd",
|
||||
"miner": "/srv/builds/igneum-wt-genesis-forward/vendor/igneum-node-gf/target/release/igneum-miner"
|
||||
}
|
||||
}
|
||||
455
docs/design/genesis-forward-harness/pass-succession.json
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
{
|
||||
"expect": "succession",
|
||||
"pass": true,
|
||||
"checks": {
|
||||
"window_filled_before_the_succession": true,
|
||||
"succession_accepted_on_submit": true,
|
||||
"carried_once_on_every_node": true,
|
||||
"successor_inherits_on_every_node": true,
|
||||
"old_key_handed_over_on_every_node": true,
|
||||
"nodes_agree_on_the_successor_weight": true,
|
||||
"refused_old_again_on_n2": true,
|
||||
"refused_old_again_on_n0": true,
|
||||
"refused_successor_that_handed_over": true,
|
||||
"scheme_1_refused_by_every_node": true,
|
||||
"locks_continue_after_the_fold": true,
|
||||
"zero_rejected_by_nodes": true,
|
||||
"sinks_agree": true
|
||||
},
|
||||
"old_key": "e4036b1e79c817f2",
|
||||
"new_key": "2f031ed35a29ced8",
|
||||
"old_blocks_before": 42,
|
||||
"succeeded_at_daa": 260,
|
||||
"carried_seen_at_daa": 266,
|
||||
"new_mined_alone": 29,
|
||||
"rows": [
|
||||
{
|
||||
"new": {
|
||||
"blocks": 37,
|
||||
"keyHash": "2f031ed35a29ced8",
|
||||
"participation": 1,
|
||||
"pubkey": "80ab38c523736ad8944c7d3375e7563ba3b0d5a3ab149bec568cf207edcc9e1ab1ee2566e06ff422a5ec4c85a4049976",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "e4036b1e79c817f2",
|
||||
"succeededTo": "",
|
||||
"voter": true
|
||||
},
|
||||
"old": {
|
||||
"blocks": 0,
|
||||
"keyHash": "e4036b1e79c817f2",
|
||||
"participation": 0,
|
||||
"pubkey": "b0fd8eebbbaad96268e5f7b8a86090ca0c723954a0a5cb81c92b633094c1742b2505c4f7cea646c5c96bb52e57400067",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "",
|
||||
"succeededTo": "2f031ed35a29ced8",
|
||||
"voter": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"new": {
|
||||
"blocks": 37,
|
||||
"keyHash": "2f031ed35a29ced8",
|
||||
"participation": 1,
|
||||
"pubkey": "80ab38c523736ad8944c7d3375e7563ba3b0d5a3ab149bec568cf207edcc9e1ab1ee2566e06ff422a5ec4c85a4049976",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "e4036b1e79c817f2",
|
||||
"succeededTo": "",
|
||||
"voter": true
|
||||
},
|
||||
"old": {
|
||||
"blocks": 0,
|
||||
"keyHash": "e4036b1e79c817f2",
|
||||
"participation": 0,
|
||||
"pubkey": "b0fd8eebbbaad96268e5f7b8a86090ca0c723954a0a5cb81c92b633094c1742b2505c4f7cea646c5c96bb52e57400067",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "",
|
||||
"succeededTo": "2f031ed35a29ced8",
|
||||
"voter": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"new": {
|
||||
"blocks": 37,
|
||||
"keyHash": "2f031ed35a29ced8",
|
||||
"participation": 1,
|
||||
"pubkey": "80ab38c523736ad8944c7d3375e7563ba3b0d5a3ab149bec568cf207edcc9e1ab1ee2566e06ff422a5ec4c85a4049976",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "e4036b1e79c817f2",
|
||||
"succeededTo": "",
|
||||
"voter": true
|
||||
},
|
||||
"old": {
|
||||
"blocks": 0,
|
||||
"keyHash": "e4036b1e79c817f2",
|
||||
"participation": 0,
|
||||
"pubkey": "b0fd8eebbbaad96268e5f7b8a86090ca0c723954a0a5cb81c92b633094c1742b2505c4f7cea646c5c96bb52e57400067",
|
||||
"strippedUntilDaa": 0,
|
||||
"succeededFrom": "",
|
||||
"succeededTo": "2f031ed35a29ced8",
|
||||
"voter": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"locks": [
|
||||
7,
|
||||
7,
|
||||
7
|
||||
],
|
||||
"locks_at_fold": 4,
|
||||
"refusals": [
|
||||
{
|
||||
"what": "w5-old -> w5-third on n2",
|
||||
"code": 3,
|
||||
"line": "1791379651.057 SUCCEED REFUSED old=e4036b1e79c817f2 new=6543d321fd61aedb daa=290 (refused: key e4036b1e79c817f2 has already handed its weight to 2f031ed35a29ced8; a key hands over once)"
|
||||
},
|
||||
{
|
||||
"what": "w5-old -> w5-third on n0",
|
||||
"code": 3,
|
||||
"line": "1791379651.072 SUCCEED REFUSED old=e4036b1e79c817f2 new=6543d321fd61aedb daa=290 (refused: key e4036b1e79c817f2 has already handed its weight to 2f031ed35a29ced8; a key hands over once)"
|
||||
},
|
||||
{
|
||||
"what": "w5-new -> w5-old on n1",
|
||||
"code": 3,
|
||||
"line": "1791379651.085 SUCCEED REFUSED old=2f031ed35a29ced8 new=e4036b1e79c817f2 daa=290 (refused: successor e4036b1e79c817f2 has itself handed its weight to 2f031ed35a29ced8; it signs nothing and can inherit nothing)"
|
||||
}
|
||||
],
|
||||
"probes": [
|
||||
{
|
||||
"node": 0,
|
||||
"code": 0,
|
||||
"line": "1791379651.097 SCHEME 1 REFUSED key=b875b095c1b5d559 index=9 (refused: vote carries signature scheme 1; the active scheme is 0 (BLS12-381); another scheme is named only by a program class the 95 percent signal moves to, and none names one)"
|
||||
},
|
||||
{
|
||||
"node": 1,
|
||||
"code": 0,
|
||||
"line": "1791379651.110 SCHEME 1 REFUSED key=b875b095c1b5d559 index=9 (refused: vote carries signature scheme 1; the active scheme is 0 (BLS12-381); another scheme is named only by a program class the 95 percent signal moves to, and none names one)"
|
||||
},
|
||||
{
|
||||
"node": 2,
|
||||
"code": 0,
|
||||
"line": "1791379651.123 SCHEME 1 REFUSED key=b875b095c1b5d559 index=9 (refused: vote carries signature scheme 1; the active scheme is 0 (BLS12-381); another scheme is named only by a program class the 95 percent signal moves to, and none names one)"
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"t": 5.6,
|
||||
"daa": 0,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"?",
|
||||
"?",
|
||||
"?"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 20.6,
|
||||
"daa": 37,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"?",
|
||||
"?",
|
||||
"?"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 35.6,
|
||||
"daa": 56,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"28/3",
|
||||
"28/3",
|
||||
"28/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 50.6,
|
||||
"daa": 71,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"28/3",
|
||||
"28/3",
|
||||
"28/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 65.6,
|
||||
"daa": 85,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"58/3",
|
||||
"58/3",
|
||||
"58/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 80.6,
|
||||
"daa": 106,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"58/3",
|
||||
"58/3",
|
||||
"58/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 95.6,
|
||||
"daa": 126,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"88/3",
|
||||
"88/3",
|
||||
"88/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 110.6,
|
||||
"daa": 141,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"118/3",
|
||||
"118/3",
|
||||
"118/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 125.6,
|
||||
"daa": 155,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"118/3",
|
||||
"118/3",
|
||||
"118/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 140.6,
|
||||
"daa": 166,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"118/3",
|
||||
"118/3",
|
||||
"118/3"
|
||||
],
|
||||
"locks": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 155.7,
|
||||
"daa": 185,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 170.7,
|
||||
"daa": 202,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 185.7,
|
||||
"daa": 213,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 200.7,
|
||||
"daa": 229,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 215.7,
|
||||
"daa": 244,
|
||||
"phase": "fill",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
3,
|
||||
3,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 233.2,
|
||||
"daa": 261,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
4,
|
||||
4,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 248.3,
|
||||
"daa": 280,
|
||||
"phase": "carry",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
4,
|
||||
4,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 263.4,
|
||||
"daa": 300,
|
||||
"phase": "after",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
5,
|
||||
5,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 278.4,
|
||||
"daa": 316,
|
||||
"phase": "after",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
5,
|
||||
5,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 293.4,
|
||||
"daa": 328,
|
||||
"phase": "after",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
6,
|
||||
6,
|
||||
6
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 308.4,
|
||||
"daa": 343,
|
||||
"phase": "after",
|
||||
"weights": [
|
||||
"120/3",
|
||||
"120/3",
|
||||
"120/3"
|
||||
],
|
||||
"locks": [
|
||||
6,
|
||||
6,
|
||||
6
|
||||
]
|
||||
}
|
||||
],
|
||||
"wall_s": 317,
|
||||
"binaries": {
|
||||
"igneumd": "/srv/builds/igneum-wt-genesis-forward/vendor/igneum-node-gf/target/release/igneumd",
|
||||
"miner": "/srv/builds/igneum-wt-genesis-forward/vendor/igneum-node-gf/target/release/igneum-miner"
|
||||
}
|
||||
}
|
||||
85
docs/design/genesis-forward.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Genesis forward-compatibility: the scheme byte, key succession, the cache rung
|
||||
|
||||
7 October 2026, 10:1x UK, the project lead's order to build mission item 8 now (`docs/analysis/mission/mission.md` section 2.8; the research in `docs/analysis/mission/future.md` sections 1.3, 7 and 10 and `docs/design/finality-in-proof.md` section 7). Branch `genesis-forward` on the node fork (from release-0.3.19-node dc141409, the merged line that carries the ladder and the W7 leave item, which ca3-v4-0318 alone does not) and `genesis-forward` on the repo. Three genesis fields, every switch never on the devnet (its digest does not move), all three set at the testnet genesis by the testnet lane, which holds the cut and re-pins. The class-group VDF's quantum fallback is flagged in spec 04 section 4.8, not built.
|
||||
|
||||
## 1. The scheme byte
|
||||
|
||||
| Item | Place | Value |
|
||||
|---|---|---|
|
||||
| The byte | `finality::Vote::sig_scheme`, `finality::KeyReveal::sig_scheme`, `finality::Succession::successor_scheme` | `SIG_SCHEME_BLS12_381` = 0 everywhere today |
|
||||
| The genesis value | `Params::sig_scheme` (override key `sig_scheme`) | 0 on every network |
|
||||
| The switch | `Params::sig_scheme_activation_daa` (override key `sig_scheme_activation_daa`; `u64::MAX` = never) | never on devnet, simnet, mainnet; 0 at the testnet genesis |
|
||||
| The digest | both fields enter once the switch is set (the 0.3.15 rule) | the devnet's digest unchanged; the testnet's moves at the cut |
|
||||
| The wire, plain | vote item tag 1 (`Vote::LEN` = 280 bytes), evidence tag 3, reveal `IGNK` + 288 hex: scheme 0 implied | byte for byte what every live network carries |
|
||||
| The wire, explicit | vote item tag 5 = `scheme \|\| vote`, evidence tag 6 = `scheme \|\| first \|\| second`, reveal `IGNS` + 2 hex of the scheme + 288 hex | written by every template once the switch is active (`encode_section_explicit_within`); a vote of any other scheme is always explicit, so a scheme the node does not run is never mistaken for one it does |
|
||||
| The active scheme | `igneum::active_sig_scheme(genesis, class)` = the scheme the program class at the sink names (`SIG_SCHEME_OF_CLASS`, a genesis table with no row today), else the genesis byte; the finality manager re-reads it at every virtual change | 0 |
|
||||
| The refusal | `FinalityManager::scheme_refusal`: a reveal is not registered, a vote is not recorded, carried or gossiped, a successor is refused; the RPC answers `refused: vote carries signature scheme 1; the active scheme is 0 (BLS12-381); another scheme is named only by a program class the 95 percent signal moves to, and none names one` | every node, whatever its switch |
|
||||
|
||||
Why a class change names the scheme: the flip is the P2 mechanism (95 percent of mining weight over a window with a floor height, the one path a consensus change takes on this chain), and the aggregated-vote format must ship first (naive ML-DSA-44 votes at 8,192 voters cost 19.4 MB a checkpoint and 57 GB a day, `future.md` 7.3; with 250x SNARK aggregation about 223 MB a day). Adding a row to the table is a code change under the class path; the byte is in every item from genesis so the row costs no fork.
|
||||
|
||||
## 2. Key succession, W5
|
||||
|
||||
| Item | Place | Value |
|
||||
|---|---|---|
|
||||
| The item | `finality::Succession` (tag 7, 297 bytes): `daa`, old key, successor key, successor scheme, the old key's signature, the successor's signature, both over `"igneum-succeed-v1/" \|\| chain_id \|\| 0 \|\| daa \|\| old \|\| new \|\| scheme` under `IGNEUM_SUCCEED_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_` | the successor's signature is its consent and its proof of possession in one |
|
||||
| The switch | `Params::finality_succession_activation_daa` (override key of the same name) | never everywhere; 0 at the testnet genesis; in the digest once set |
|
||||
| Submission | `submitFinalitySuccession` (RPC op 158, gRPC 1131/1132, wRPC, `igneum-miner succeed <grpc url> <old label> <new label>`) | carried after the leaves in the templates of the node that holds it; no p2p gossip kind (the successor's own node mines it) |
|
||||
| The fold | `successions_at` (deterministic like `leaves_at`: the lowest-DAA carrier in C's past dates it) and `fold_successions` inside `voters_at`: the old key's window blocks are credited to the successor as they stand (count and oldest block), the old key leaves the table, a ban or a leave on the old key lands on the successor; the successor's presence counts the old key's carried votes | from the first checkpoint whose past holds the carrier; the window inherited, not a fresh one |
|
||||
| Once | one record per old key: a second succession from a key that handed over is refused (`already handed its weight to`), a successor that has itself handed over is refused (`has itself handed`), which also refuses every cycle; a chain old to new to newer is legal | a record outlives the weight it moved by one window, then is trimmed |
|
||||
| After | the old key's votes are refused (`handed its weight to`), never counted, never carried | the old key is dead |
|
||||
| The report | `getFinalityWeights`: `succeededFrom` on the successor's row, a weightless row for the old key with `succeededTo` | both nodes of the unit test agree on every voter list |
|
||||
|
||||
Not in this round: a p2p gossip kind for successions (the leave's shape, protocol version bump); the app's "rotate key" button, which signs the item from the old key's label and restarts the miner under the new label (the `succeed` subcommand is the primitive); the aggregated-vote format.
|
||||
|
||||
## 3. The cache rung beside N
|
||||
|
||||
| Item | Place | Value |
|
||||
|---|---|---|
|
||||
| The rung | `igneum::CacheRung { mib, admissible }` as `LatencyLadder::cache_rung` (override key `latency_ladder_cache_rung`) | `{512, false}` at genesis; a power of two above the 256 MiB genesis cache |
|
||||
| The signal | `LadderSignal::Cache` = both ladder bits set (header version 0xc000; until today both set was "no signal" and no node ever stamped it, so no block written so far changes meaning); `IGNEUM_LADDER_SIGNAL=cache` | code 3 in `PowEpochInfo::latency_ladder_signal` |
|
||||
| The rule | `latency_ladder_step_signalled_with_cache`: the cache step moves 0 to 1 when every one of the newest seven windows reaches 90 percent for the cache rung, the rung is admissible and the cool-down holds (the oldest window begins after the last decision, shared with N); never back; never in the same decision as N (one signal per block, exclusive shares) | `LadderState::cache_step` |
|
||||
| The digest | the rung's size and flag enter with the ladder, once `latency_ladder_activation_daa` is set | the testnet's digest moves at the cut |
|
||||
| The RPC | `powEpoch.latencyLadderCacheStep`, `nextLatencyLadderCacheStep`, `latencyLadderCacheMib`, `nextLatencyLadderCacheMib`, `latencyLadderCacheBps`, `latencyLadderCacheWeakestBps`, `latencyLadderCacheAdmissible` (proto fields 37 to 43); the daemon's ladder line (`cache [512 MiB]`) | the rung visible on every node |
|
||||
| The gate | `admissible` in the genesis list, false until measured: the cold verify of one warp with the 512 MiB cache on the reference core with its SMT sibling loaded under 10 ms (the verifier reads the cache, so this is the bound), the day-cache build on a 2019-class core under twice today's, and the 8 GB tier still holding dataset, cache and the prover footprint | the rule never enters an inadmissible rung (tested) |
|
||||
|
||||
Why beside N and not a seventh N rung: the six approved rungs and their indices stand (the project lead, 7 October 2026, 09:3x UK); a rung inserted in the list would either sit behind the three inadmissible rungs (unreachable) or shift the approved indices. A second lever on the same signal carrier keeps the list as approved and the cool-down shared.
|
||||
|
||||
Owed with the measurement: the engine's consumption of `cache_mib` (`EpochSeeds` carries `shadow_reps` today and no cache size; the pack and the three hosts build a 256 MiB cache), so the flag stays false until the path exists and is measured. Per tier what the rung means: every tier from 8 GB holds a 512 MiB cache; the day-cache build doubles (about 0.7 s on the reference core today, approximate, from the class v4 fill line); the Apple tier's unified memory holds it; a pool user does nothing.
|
||||
|
||||
## 4. The class-group VDF under a quantum computer
|
||||
|
||||
Flagged in spec 04 section 4.8, not sized: Shor computes the class-group order, which removes the sequentiality assumption of the Wesolowski VDF, so an attacker with a cryptographically relevant quantum computer grinds the hourly seed (a liveness nuisance against the lottery, not a safety break; finality rests on the vote keys of section 1). The fallback is a hash-chain delay behind the same version byte, designed when the scheme flip is scheduled.
|
||||
|
||||
## 5. Gates
|
||||
|
||||
| Gate | Where | Result |
|
||||
|---|---|---|
|
||||
| The digest test | `consensus_digest_covers_every_consensus_field_and_nothing_else` (30 edits; the scheme byte and the cache rung alone move nothing), `override_params_carry_the_genesis_forward_fields_and_the_digest_moves_only_when_set` | section 6 |
|
||||
| Scheme 1 refused by every node until the signal | unit: `a_vote_reveal_or_successor_of_another_signature_scheme_is_refused_until_a_class_names_it` (RPC, in a block, a reveal, a successor; the explicit template form); fast-time: `probe-scheme` on three nodes | section 6 |
|
||||
| A succession carried once and refused twice | unit: `a_key_hands_its_window_to_a_successor_once_and_a_second_succession_is_refused` (two nodes); fast-time: `infra/fast-time/key-succession.mjs` (the known-failed case `--expect no-succession` first) | section 6 |
|
||||
| The ladder rung visible in the RPC | `powEpoch.latencyLadderCache*` on `getBlockTemplate`, the daemon line; unit: `latency_ladder_rule` (an inadmissible cache rung never entered; with the flag, 90 percent in seven windows enters it, N still steps after it, one rung, never back) | section 6 |
|
||||
| The codec | `scheme_byte_and_succession_items_round_trip_and_an_older_decoder_stops_at_them` | section 6 |
|
||||
|
||||
## 6. Results (7 October 2026, 12:5x to 14:3x UK; igneum-build-1 for the gate build, the harness and the digests, igneum-build-2 for the suites)
|
||||
|
||||
| Gate | Run | Result |
|
||||
|---|---|---|
|
||||
| The digest test | `cargo test --release -p kaspa-consensus-core --lib` on build-2 at 75810130 | 124 passed, 0 failed, 2 ignored (`consensus_digest_covers_every_consensus_field_and_nothing_else` with 30 edits, `override_params_carry_the_genesis_forward_fields_and_the_digest_moves_only_when_set`, `latency_ladder_rule` with the cache rung) |
|
||||
| The 60x keeper test | `fast_time_60x_file_is_the_devnet_at_60x` on build-2 against the completed file (repo 9c9a1f52) | 1 passed (the file had lacked 17 fields on master, `emission` and `proving_consensus_verify_daa` among them; the box mirrors carry no `infra/`, so the test skips there unless the file is shipped) |
|
||||
| Scheme 1 refused by every node | unit `a_vote_reveal_or_successor_of_another_signature_scheme_is_refused_until_a_class_names_it`; fast-time `probe-scheme` on three nodes, both harness cases | green in the suite runs; every node: `SCHEME 1 REFUSED ... (refused: vote carries signature scheme 1; the active scheme is 0 (BLS12-381); another scheme is named only by a program class the 95 percent signal moves to, and none names one)` |
|
||||
| A succession carried once and refused twice | unit `a_key_hands_its_window_to_a_successor_once_and_a_second_succession_is_refused` (two nodes); fast-time `infra/fast-time/key-succession.mjs` on build-1 (3 nodes, one CPU miner each, override-60x with the three switches at 0) | known-failed case first (`--expect no-succession`, 13:04 to 13:13 UK, `genesis-forward-harness/known-failed-no-succession.json`): FAIL as it must on every carried-once check with the probe, the locks and the sinks holding. Pass case (`--expect succession`, node bdb34f62, 13:23 to 13:28 UK, `pass-succession.json`): PASS, every check holds: w5-old held 42 blocks at DAA 260; the succession accepted on n2 at DAA 261 and carried by a block at DAA 266 on every node; at the fold (DAA 290) every node reads w5-new 37 blocks (7 mined alone) and w5-old 0 with `succeededTo`; w5-old to w5-third refused on n2 and n0 (`already handed`), w5-new to w5-old refused on n1 (`has itself handed`); locks 4 to 7 on every node after the fold; 0 rejected blocks, sinks agree; 317 s wall |
|
||||
| The cache rung visible in the RPC | `powEpoch.latencyLadderCache*` (proto fields 37 to 43); the daemon's ladder line | the testnet-shaped file prints `rungs 27, 35, 53, [88], [173], [267] shadow passes, cache [512 MiB] beside them` (`digest-testnet-shape.log`) |
|
||||
| The codec | `scheme_byte_and_succession_items_round_trip_and_an_older_decoder_stops_at_them` | green in the suite runs |
|
||||
| The digest, cross-binary | igneumd 75810130 against the 0.3.20 base dc141409 (bs0319's build), both with no file, devnet suffix 973 | both `079d8a7e736abbd4` (`digest-no-file.log`, `digest-base-dc141409.log`): the three fields at never move nothing; the ladder alone at 0 reads `772f9f8e3ef59ca1`, the testnet-shaped file (ladder at 0, scheme switch at 0, succession at 0, the cache rung) `60e842a738c7dea0`, on devnet params; the testnet lane's own number comes from `TESTNET_PARAMS` at its cut. The 0.3.18 line's `c562d70e` is behind the decimals, tail-emission and subsidy fields of the 0.3.19 and 0.3.20 lines, not behind these |
|
||||
| The gate build | `tools/build-remote.sh --priority gate` on build-1 at bdb34f62 | igneumd 57,554,336 B sha256 6d0aa37b..., igneum-miner 10,240,768 B sha256 69fc3c63... (commit string carried) |
|
||||
| The consensus suite | `cargo test --release -p kaspa-consensus` (all targets) on build-2 at f95178a1, twice | 114 passed, 0 failed, 3 ignored in the lib binary both times, the two moved targets 1 each; `cargo check -p kaspad` clean. Before the fix below the lib binary failed 1 of 114 on one of the two-node tests in three of four runs (the succession test once, the pre-existing F23 test twice), node 1 refusing block 61 at DAA 60 with `UnexpectedDifficulty`, the two nodes' bits about 17,000 apart in the mantissa |
|
||||
|
||||
Faults found on the way, both mine: (1) rule v3's frozen table stood at the lock before the carrier, where the old key still weighed and signed nothing, so nothing locked after the fold (fixed in `frozen_table`, 9322cc1d); (2) the template read the explicit-form switch from the process-wide static that only the daemon installs, so `TestConsensus` wrote the plain form (the manager holds the activation now, daa61847).
|
||||
|
||||
### 6a. The two-node refusal: found and fixed (f95178a1)
|
||||
|
||||
The probe on build-2: F23 alone three times, green each time; the three two-node tests together three times, green each time; so the refusal needed the rest of the lib binary. The cause: `consensus/src/consensus/services.rs` built the difficulty manager with `igneum::pow_epoch_blocks()`, the process-wide static, where every other argument of that constructor comes from `params`; two pruning-proof tests in the same binary (`igneum_m20_tests.rs:27`, `igneum_pow.rs:346`) install a 60-block schedule process-wide, so a `TestConsensus` built inside that window read a 60-block epoch into its difficulty manager while its partner read 3,600, the two disagreed on `reference_window` from DAA 60, and the second node refused block 61, the exact block of every refusal. The fix is the manager's own field, `params.pow_epoch_blocks`. The node's behaviour is unchanged (the daemon installs the static from the same params before building the consensus); the class is the static-at-construction read, the sibling of the signal-table race the node lane moved two installing tests for on 7 October 2026. The test helper now names the node and the block it refuses, which is what found it.
|
||||
|
||||
## 7. For the testnet lane
|
||||
|
||||
Override keys and testnet values: `sig_scheme: 0`, `sig_scheme_activation_daa: 0`, `finality_succession_activation_daa: 0`, `latency_ladder_cache_rung: {"mib": 512, "admissible": false}` (the six N rungs unchanged). Digest: with the ladder already active from genesis the cache rung's two fields enter after the six rungs' fields; the scheme switch adds two fields, the succession switch one. `print_testnet_object` prints the four keys.
|
||||
|
|
@ -2434,3 +2434,35 @@ Same rule as the counts above. 167 entries.
|
|||
|
||||
The bucket "Decided or closed by rule" counts each entry once: M8, M11 and P16 move there from Answered, so Answered is 28 less those three plus M16 and E17, as the table states; the sum of the rows is 167 with M8, M11, P16 and F3, F17 counted in Decided only.
|
||||
|
||||
|
||||
## Genesis forward-compatibility entries (7 October 2026, mission item 8, branch `genesis-forward`)
|
||||
|
||||
The three genesis fields of `docs/analysis/mission/mission.md` section 2.8, built on the node fork branch `genesis-forward` (from release-0.3.19-node dc141409) and the repo branch `genesis-forward`; the design and the gates in `docs/design/genesis-forward.md`. Every switch is never on the devnet (its digest c562d70e... does not move); the testnet genesis sets all three (the testnet lane re-pins and re-digests).
|
||||
|
||||
### GF1. A post-quantum signature scheme would need a hard fork, and every vote key is a public BLS12-381 point
|
||||
"When a quantum computer comes, every BLS vote key is forged and the chain has no way to change the scheme without a fork of the kind you say you never need."
|
||||
|
||||
Status: Fixed (7 October 2026). `sig_scheme` byte in every vote item and key reveal (`finality::SIG_SCHEME_BLS12_381` = 0), `Params::sig_scheme` and `Params::sig_scheme_activation_daa` in the digest once set; any other byte refused by every node (`scheme_refusal`) until a program class the 95 percent signal moves to names it (`igneum::sig_scheme_of_class`, empty today). Gate: the digest test `override_params_carry_the_genesis_forward_fields_and_the_digest_moves_only_when_set` and `consensus_digest_covers_every_consensus_field_and_nothing_else` (30 edits); a scheme-1 vote refused over RPC, in a block, and as a successor's scheme (`a_vote_reveal_or_successor_of_another_signature_scheme_is_refused_until_a_class_names_it`); the fast-time probe on three nodes (`infra/fast-time/key-succession.mjs`, `probe-scheme`).
|
||||
|
||||
Answer: The byte costs nothing now and a fork later. The flip is a class change (spec 03 W1 as amended): the aggregated-vote format ships first (naive ML-DSA-44 votes at 8,192 voters cost 57 GB a day, `future.md` 7.3), the scheme second, both by the 95 percent signal with a floor height. Per tier at migration: a home miner on any card runs the updater and signs one succession item (GF2); nothing hardware-specific, the signature runs on the CPU.
|
||||
|
||||
### GF2. A vote key cannot move: a miner who changes keys re-earns 30 days of weight, and so does the post-quantum migration
|
||||
"Your weight is bound to a key. Rotate it, lose a month. So nobody rotates, and the one day everyone must rotate, finality pauses for a month."
|
||||
|
||||
Status: Fixed (7 October 2026). W5 key succession implemented (spec 03 W5 as amended): a succession item signed by both keys, carried in blocks after the leaves, folds the old key's window blocks and forfeit term into the successor from the first checkpoint whose past holds the carrier (`successions_at`, `fold_successions`); once per key, a second succession refused, a successor that has itself handed over refused. Behind `finality_succession_activation_daa`. Gate: carried once and refused twice in the unit test on two nodes and in the fast-time harness.
|
||||
|
||||
Answer: The successor inherits the window, not a fresh one, so a key rotation costs no weight and the migration of GF1 is one item per key. The old key signs nothing after; a buyer of a key gets the clean transfer (spec 3.11.5) and the seller's copy is worthless.
|
||||
|
||||
### GF3. A 256 MB on-chip cache makes the lottery hash 2 to 3x cheaper for the card that has it, and the cache size is a constant
|
||||
"The dataset is built from a 256 MiB cache. A datacentre part with a 256 MB last-level cache keeps the whole cache on die and skips the memory. Consumer cards cannot."
|
||||
|
||||
Status: Fixed as a genesis lever, measurement owed (7 October 2026). The latency ladder carries one cache rung beside N (`LatencyLadder::cache_rung`, 512 MiB, `LadderSignal::Cache` = both ladder bits, the same 90 percent in seven windows, one rung, never back, in the digest with the ladder); inadmissible at genesis until the verifier bound and the 8 GB tier are measured (design doc section 3); visible in `getBlockTemplate`'s `powEpoch` (`latencyLadderCacheStep`, `latencyLadderCacheMib`, `latencyLadderCacheBps`, `latencyLadderCacheAdmissible`) and in the daemon's ladder line.
|
||||
|
||||
Answer: Consumer LLC is 96 to 128 MB today and datacentre 256 MB (`chip-model-v3`, approximate), so the shortcut is a datacentre card's today and a consumer card's in a generation or two. The rung lets the miners double the cache by signal the day the shortcut shows on the hash-rate charts, without a fork; the measurement that admits it is the same verifier bound as every N rung. Per tier: a 512 MiB cache is held by every tier from 8 GB up; the day-cache build doubles (about 0.7 s on the reference core today, approximate); the verifier reads the cache, not the dataset, so the 10 ms bound decides.
|
||||
|
||||
### GF4. The class-group VDF falls to the same quantum computer
|
||||
"Shor computes the class-group order; your epoch seed is then grindable."
|
||||
|
||||
Status: Conceded, flagged in spec 04 section 4.8 (7 October 2026); not sized. The fallback is a hash-chain delay behind the same version byte that moves the signature scheme; designed when the scheme flip is scheduled.
|
||||
|
||||
Answer: A grindable hourly seed is a liveness nuisance against the lottery, not a safety break: finality rests on the vote keys (GF1), the seed on the VDF. The two flip together by one class change.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 613 KiB After Width: | Height: | Size: 676 KiB |
|
Before Width: | Height: | Size: 634 KiB After Width: | Height: | Size: 611 KiB |
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 377 KiB |
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 386 KiB |
|
|
@ -207,7 +207,7 @@ Implemented for the prototype size; Designed for genesis.
|
|||
|
||||
A load reads one 4-byte word at `src AND MASK`. Every load in every emitted kernel has exactly this form; the static check in `TESTS.md` section 5 is part of conformance (section 1.15). Because item values do not depend on the dataset size (section 1.8.5), the 1 GiB vectors remain valid for words below 2^28 at any larger size.
|
||||
|
||||
Growth beyond genesis is in section 1.13; under program class v3 the cache follows the dataset's doublings (1.13.3) and the verifier holds 256 MiB, then 512 MiB from year 4 and 1 GiB from year 12.
|
||||
Growth beyond genesis is in section 1.13; under program class v3 the cache follows the dataset's doublings (1.13.3) and the verifier holds 256 MiB, then 512 MiB from year 4 and 1 GiB from year 12. Beside the schedule the latency ladder (`docs/design/latency-ladder.md`) carries one cache rung (genesis forward-compatibility, 7 October 2026, `docs/design/genesis-forward.md` section 3): `LatencyLadder::cache_rung`, 512 MiB, entered by the same rule as a shadow rung (90 percent of blue blocks with both ladder bits set in each of seven consecutive windows, one decision per seven windows, one rung, never back), because a consumer last-level cache at the cache size gives that card's owners a 2 to 3x shortcut (`chip-model-v3`; consumer LLC 96 to 128 MB today, datacentre 256 MB). Its gate is the rung's `admissible` flag in the genesis list, false until the cold verify with the larger cache on the reference core with its sibling loaded is measured under 10 ms, the day-cache build on a 2019-class core under twice today's, and the 8 GB tier still holds dataset, cache and the prover footprint; the engine's consumption of the larger cache is owed with that measurement, and the rule never enters the rung before the flag is set.
|
||||
|
||||
## 1.6 Register initialisation
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ Two statements frame everything below. Finality is miner-only and self-contained
|
|||
|
||||
## 3.1 Weight
|
||||
|
||||
- **W1.** Every block header names a vote key by `vote_key_hash` (section 2.4): the hash of a BLS12-381 G1 compressed public key. The first block that uses a key reveals the key in its coinbase payload. A header whose `vote_key_hash` has never been revealed is valid; the key simply cannot vote until it is revealed.
|
||||
- **W1.** Every block header names a vote key by `vote_key_hash` (section 2.4): the hash of a BLS12-381 G1 compressed public key. The first block that uses a key reveals the key in its coinbase payload. A header whose `vote_key_hash` has never been revealed is valid; the key simply cannot vote until it is revealed. Every key reveal and every vote item carries a signature scheme byte, `sig_scheme` (genesis forward-compatibility, 7 October 2026, `docs/design/genesis-forward.md`): the genesis value is 0, BLS12-381 in the minimal-public-key setting, and a node refuses any reveal, vote or successor whose byte is not the active scheme. The active scheme is the genesis byte until a program class that the 95 percent class signal moves to names another (`igneum::sig_scheme_of_class`, a genesis table with no row today), so a post-quantum scheme arrives by signal, never by a fork; the aggregated-vote format (`docs/analysis/mission/future.md` 7.3) ships before the flip.
|
||||
- **W2.** Weight is counted in blocks and denominated in past-median time (rule of 3 October 2026, ledger M14 and F14, round 3; the simulated form is kept below). Cut the trailing 30 days of past-median time before C, `(mt(C) - 2,592,000 s, mt(C)]`, into 43,200 buckets of 60 s; a blue block in C's past falls in the bucket that holds its own past-median time. The weight of key k at C is the sum over buckets of k's share of the blue blocks in that bucket (an empty bucket contributes 0 to everyone). A bucket is worth 1 whatever it holds, so 50x the blocks in one minute is one minute of weight, and a retarget lag can neither inflate a key's count nor age the window. No damping, no cap, no floor. Blocks are the only thing in proof of work that cannot be forged, so weight is counted in blocks; time is the denominator because blocks per unit of DAA time is exactly what a lagging controller lets a renter buy (section 2.3; `docs/review/round-3-2026-10-03.md`, "Tonight's devnet"). As simulated (`sim/results_v2.md`, every run): the number of blue blocks in C's past whose header names k and whose DAA score is in `(daa(C) - 2,592,000, daa(C)]`. The two forms agree whenever the controller holds the target; O-3.14 runs the simulation under both with the DAA in the loop and confirms or reverts this rule at gate 3. The damped rule of version 1 was removed because its 2x cap was defeated by splitting into free keys (`sim/results.md` table C; `docs/bench-log.md` finality_sim entry).
|
||||
- **W3.** Dust: a key with fewer than 100 blue blocks in the window (a block count, not bucket weight) is not a voter and is in no denominator. Measured consequence (`sim/results_v2.md` A and G): every honest key in a 1,000-key Pareto network clears 100 blocks by day 20 from zero history and the smallest new keys need up to 25 days after a doubling; a 9x renter's victims lose about one point to dust (B).
|
||||
- **W4.** Steady state: weight equals hashrate. Simulated correlation 1.00000 at day 60, Gini equal to three figures, weight-to-hash ratio within 0.82 to 1.12 for every key (`sim/results_v2.md` A).
|
||||
- **W5.** Key succession: a message signed by the old key naming a new key, included in any block, moves the old key's weight history to the new key once. The old key is dead thereafter: its later blocks earn nothing and its votes are invalid.
|
||||
- **W5.** Key succession: a succession item signed by both keys (the old key hands over, the successor consents and proves possession in one signature) over `(chain_id, daa, old key, successor key, successor scheme)`, carried in any block, moves the old key's weight to the successor once. The fold is a function of the chain, as the equivocation ban (3.6) and the leave (W7) are: at checkpoint C the old key's blue blocks in the window are credited to the successor exactly when some block in C's past carries the succession and the rule is active at C, the successor inherits the window as it stands (the count and its oldest block, never a fresh window), and a ban or a leave on the old key lands on the successor (the forfeit term inherited). The old key is dead thereafter: its votes are refused and never carried, and no node records a second succession from it, nor one naming as successor a key that has itself handed over (which also refuses every cycle). A chain old to new to newer is legal, each key handing over once. Implemented 7 October 2026 (`docs/design/genesis-forward.md` section 2; behind `finality_succession_activation_daa`, 0 on the testnet).
|
||||
- **W6.** Keys are free. Nothing in the protocol prices a vote key and the devnet launcher mints one per worker process (ledger F17, round 3). Weight is the only Sybil-resistant quantity in this specification, so every rule that draws from the voter population draws by weight and never per key: W2 (no damping, no per-key cap), the shard sortition of section 7.2 (drawn by weight since 3 October 2026) and the sub-user sortition of S2. A rule that counts keys is a rule a splitter wins.
|
||||
|
||||
The headline arithmetic (W2 with constant hashrate): an attacker with share a of hashrate for t days holds weight share `(t/30) x a/(1+a)`, verified by simulation to 0.04 points for a = 1 and 2 (`sim/results_v2.md` B).
|
||||
|
|
@ -166,9 +166,10 @@ Status of this section: Implemented in `vendor/igneum-node` (reading guide in `d
|
|||
|
||||
| Clause | Implementation | Departure or gap |
|
||||
|---|---|---|
|
||||
| W1 | `vote_key_hash` = BLAKE2b (domain `IgneumVoteKeyHash`) of the 48-byte compressed G1 key. The key is revealed with a proof of possession in the miner's coinbase extra data (`IGNK` plus 288 hex characters) and a node registers it only when the hash matches the header. A vote carries the public key too, so a key that votes is revealed by its vote | The reveal is hex, not binary, because the template RPC carries extra data as a UTF-8 string. Mainnet should carry the reveal in a dedicated field or transaction |
|
||||
| W1 | `vote_key_hash` = BLAKE2b (domain `IgneumVoteKeyHash`) of the 48-byte compressed G1 key. The key is revealed with a proof of possession in the miner's coinbase extra data (`IGNK` plus 288 hex characters, scheme 0 implied; or `IGNS` plus two hex characters of the scheme byte plus the 288, the explicit form) and a node registers it only when the hash matches the header and the scheme byte is the active one. A vote carries the public key too, so a key that votes is revealed by its vote. The vote item carries its scheme byte as item tag 5 (`scheme \|\| vote`; tag 1 is the plain scheme-0 form every live network carries), evidence as tag 6; under `sig_scheme_activation_daa` every template writes the explicit forms (7 October 2026) | The reveal is hex, not binary, because the template RPC carries extra data as a UTF-8 string. Mainnet should carry the reveal in a dedicated field or transaction |
|
||||
| W2 | Blue blocks per key in `(daa(C) - window, daa(C)]`, counted along C's selected chain through every chain block's mergeset blues, C included | O(window) per checkpoint: fine at the devnet window of 7,200 DAA seconds, not at 2,592,000. Mainnet needs an incremental window kept per chain block |
|
||||
| W3, W5 | Dust excludes a key from the voter list and from both denominators | Key succession (W5) is not implemented |
|
||||
| W3 | Dust excludes a key from the voter list and from both denominators | |
|
||||
| W5 | `Succession` item (tag 7, 297 bytes: daa, old key, successor key, successor scheme byte, both signatures under `IGNEUM_SUCCEED_V1_...`), submitted over `submitFinalitySuccession` by the successor's miner, carried after the leaves in the templates of every node that holds it, one record per old key; `successions_at` dates it from the lowest-DAA carrier in C's past and `voters_at` folds the old key into the successor (`fold_successions`), bans and leaves with it; the successor's presence counts the old key's carried votes; `getFinalityWeights` shows `succeededFrom` on the successor and a weightless `succeededTo` row for the old key. Behind `finality_succession_activation_daa` (never on the devnet, 0 on the testnet). Tests: the unit test `a_key_hands_its_window_to_a_successor_once_and_a_second_succession_is_refused` (two nodes agree on every voter list, the successor locks on the inherited weight, A to D and C to A refused) and the fast-time harness `infra/fast-time/key-succession.mjs` | Successions travel in blocks only (no p2p gossip kind; the successor's own node carries it); a record outlives the weight it moved by one window and is then trimmed, so a key that handed over could hand over again two windows later with nothing left to hand |
|
||||
| C1 | Checkpoint i is the lowest selected-chain block with blue score at least 30 i (blue scores along the chain can skip values), determined when the sink's blue score reaches 30 i + d, d = 20 on devnet. Re-determination (branch `fud-consensus`, 4 October 2026 night, ledger F24): after every virtual change, every unlocked record whose block is no longer a chain ancestor of the sink is determined again on the new chain (`on_virtual_changed`, "re-determined" log line); the certificate held over the old block is dropped, the fold clock restarts, and the certificates kept pending over the new block (`pending_certificates`, at most 4 per index, indices up to 64 ahead of the next determination) are verified. A locked record is never revisited. Unit test `reorg_past_an_unlocked_checkpoint_re_determines_it_and_verifies_the_pending_certificate` (a 6-block side chain's certificate is pending with no conflict, the 15-block side chain overtakes, index 13 is re-determined and locks from it; a block with the wrong blue score is refused) and `a_locked_checkpoint_pins_the_chain_and_a_certificate_against_it_conflicts` (a side chain twice as long does not become the sink past a lock, the certificate against the lock is the one conflict) | d = 20 is below the placeholder 60; the devnet reorg-depth distribution that sets d has not been recorded. Measured in `docs/bench-log.md`, "round-4 consensus items" (reorg run) |
|
||||
| C2 | BLS signature over `"igneum-vote-v1/" \|\| chain_id \|\| 0 \|\| index \|\| hash(C_i)` under `IGNEUM_VOTE_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_`; the chain id is the prefixed network name (`igneum-devnet`, `igneum-devnet-7`); votes are p2p message 70 and ride in the coinbase extra data of every block | |
|
||||
| C3 | Certificate = index, checkpoint, voter count, signer bitmap over the canonical voter list (keys above dust and not stripped, sorted by key hash), aggregate signature, aggregator key hash and sortition proof. Every template carries the certificates not yet in its past | The validity rule (a block whose selected chain misses a certified checkpoint is invalid) is NOT enforced; only fork choice (F1, F2) is |
|
||||
|
|
|
|||
|
|
@ -103,4 +103,6 @@ Decision at gate 3 with the devnet, where the time to first block after an epoch
|
|||
|
||||
## 4.8 Open items from the prototype
|
||||
|
||||
Flagged, not sized (genesis forward-compatibility, 7 October 2026, `docs/analysis/mission/future.md` 7.2 and `docs/design/genesis-forward.md` section 4): a cryptographically relevant quantum computer computes the class-group order by Shor, which removes the sequentiality assumption of the Wesolowski VDF, so an attacker with one grinds the hourly program seed; the fallback is a hash-chain delay behind the same version byte that moves the signature scheme (a class change by the 95 percent signal), designed and sized when the scheme flip is scheduled, not before. Nothing in the pipeline of 4.3 and 4.4 changes today.
|
||||
|
||||
Carried into section 6: external review of `classgroup.rs` against chiavdf (O-4.1); reference core choice (O-4.2); whether `C(e)` must be certified and the header field (O-4.3); the fallback (O-4.4); HashPrime layout, carrying D in the proof, compact form encoding (O-4.5); reduction only when `a` exceeds 8 limbs as chiavdf does, a further speedup to port (O-4.6); no fuzzing of `deserialize` on hostile bytes beyond validity checks, no measurement on NVIDIA or AMD hosts' CPUs (O-4.7).
|
||||
|
|
|
|||
57
infra/build-server/overlap-browser.sh
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env bash
|
||||
# A headless Chromium for the text-overlap sweep (tools/ci/overlap-check.mjs) on a build box, without root. The build user
|
||||
# has no sudo, so Playwright's `install --with-deps` cannot add the X and GTK libraries Chromium links; this script downloads
|
||||
# the same Ubuntu packages with apt-get download (no root needed), unpacks them into a sysroot and points LD_LIBRARY_PATH
|
||||
# at it, plus two font packages behind a private fontconfig file (the box ships no fonts at all: fc-list is empty, and
|
||||
# text with no font has no width). Idempotent; re-run after a box re-provision.
|
||||
#
|
||||
# infra/build-server/overlap-browser.sh on the box (run-from-mac.sh copies and runs it; or ssh and run by hand)
|
||||
# . /srv/builds/_bin/overlap/env.sh what a caller sources before `node tools/ci/overlap-check.mjs`
|
||||
#
|
||||
# Installed 7 October 2026 on igneum-build-2 (Ubuntu 24.04.5, node 22): playwright 1.56 with chromium 1194 (headless shell
|
||||
# and full), 36 packages in the sysroot, Liberation and DejaVu fonts. A page with 20 px sans-serif text measured 23 px high
|
||||
# through it, so glyphs have real metrics there.
|
||||
set -euo pipefail
|
||||
ROOT="${OVERLAP_ROOT:-/srv/builds/_bin/overlap}"
|
||||
PW_VERSION="${PW_VERSION:-1.56}"
|
||||
mkdir -p "$ROOT/debs" "$ROOT/sysroot" "$ROOT/fonts" "$ROOT/fc-cache"
|
||||
cd "$ROOT"
|
||||
[ -f package.json ] || npm init -y >/dev/null
|
||||
if ! node -e "require('playwright')" 2>/dev/null; then npm i --no-audit --no-fund "playwright@$PW_VERSION" | tail -1; fi
|
||||
# the browser download (Playwright keeps it under ~/.cache/ms-playwright; a second run finds it and does nothing)
|
||||
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=1 npx playwright install chromium 2>&1 | tail -1 || true
|
||||
PKGS="libatk1.0-0t64 libatk-bridge2.0-0t64 libatspi2.0-0t64 libx11-6 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libxcb1
|
||||
libasound2t64 libxrender1 libxau6 libxdmcp6 libwayland-server0 libwayland-client0 libcups2t64 libcairo2 libpango-1.0-0 libpangocairo-1.0-0
|
||||
libpangoft2-1.0-0 libharfbuzz0b libfribidi0 libthai0 libdatrie1 libpixman-1-0 libxcb-render0 libxcb-shm0 libavahi-client3 libavahi-common3
|
||||
libxi6 libfontconfig1 libgraphite2-3 fonts-liberation fonts-dejavu-core"
|
||||
cd "$ROOT/debs"
|
||||
# shellcheck disable=SC2086
|
||||
apt-get download $PKGS 2>&1 | grep -v '^Get:' | tail -1 || true
|
||||
for d in ./*.deb; do dpkg -x "$d" "$ROOT/sysroot"; done
|
||||
find "$ROOT/sysroot" -name '*.ttf' -exec cp -n {} "$ROOT/fonts/" \;
|
||||
cat >"$ROOT/fonts.conf" <<EOF
|
||||
<?xml version="1.0"?><!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<fontconfig><dir>$ROOT/fonts</dir><cachedir>$ROOT/fc-cache</cachedir>
|
||||
<alias><family>sans-serif</family><prefer><family>Liberation Sans</family></prefer></alias>
|
||||
<alias><family>serif</family><prefer><family>Liberation Serif</family></prefer></alias>
|
||||
<alias><family>monospace</family><prefer><family>Liberation Mono</family></prefer></alias></fontconfig>
|
||||
EOF
|
||||
cat >"$ROOT/env.sh" <<EOF
|
||||
export LD_LIBRARY_PATH=$ROOT/sysroot/usr/lib/x86_64-linux-gnu\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}
|
||||
export FONTCONFIG_FILE=$ROOT/fonts.conf
|
||||
export NODE_PATH=$ROOT/node_modules\${NODE_PATH:+:\$NODE_PATH}
|
||||
export IGNEUM_PLAYWRIGHT_DIR=$ROOT
|
||||
EOF
|
||||
# shellcheck disable=SC1091
|
||||
. "$ROOT/env.sh"
|
||||
SHELL_BIN="$(ls -d "$HOME"/.cache/ms-playwright/chromium_headless_shell-*/chrome-linux/headless_shell | tail -1)"
|
||||
if ldd "$SHELL_BIN" | grep -q 'not found'; then echo "overlap-browser: libraries still missing:"; ldd "$SHELL_BIN" | grep 'not found'; exit 1; fi
|
||||
cat >"$ROOT/smoke.cjs" <<'EOF2'
|
||||
const { chromium } = require("playwright");
|
||||
(async () => { const b = await chromium.launch({ args: ["--no-sandbox"] }); const p = await b.newPage();
|
||||
await p.setContent('<p id=a style="font:20px sans-serif">Hello overlap world</p>');
|
||||
const h = await p.evaluate(() => document.getElementById("a").getBoundingClientRect().height); await b.close();
|
||||
if (!(h > 15 && h < 40)) { console.error("overlap-browser: text has no metrics (height " + h + ")"); process.exit(1); }
|
||||
console.log("overlap-browser: ready at " + __dirname + " (20 px text measures " + h + " px; source " + __dirname + "/env.sh)"); })();
|
||||
EOF2
|
||||
cd "$ROOT" && node smoke.cjs
|
||||
229
infra/fast-time/key-succession.mjs
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
#!/usr/bin/env node
|
||||
// Genesis forward-compatibility (mission item 8, docs/analysis/mission/mission.md 2.8; docs/design/genesis-forward.md):
|
||||
// the fast-time gate for the W5 key succession and the sig_scheme byte, on a 3-node network with one real CPU miner
|
||||
// per node, the class-v4-signal.mjs shape. Ports 29720 and up, network igneum-devnet-972, data /tmp/igneum-fast-time-w5;
|
||||
// override-60x.json with CPU genesis bits, finality v3, the leave item, the key succession and the explicit scheme
|
||||
// forms all active from DAA 0.
|
||||
//
|
||||
// The gate, in order (every check is a chain-side fact read from the nodes, never a rate or a process name):
|
||||
// 1. the window fills: the old key (node 2's miner, label w5-old) holds blocks on every node's weight table;
|
||||
// 2. carried once: node 2's miner stops (no leave), `igneum-miner succeed` hands w5-old to w5-new, a miner starts
|
||||
// under w5-new; every node logs the succession carried by a block, every node's getFinalityWeights shows w5-new
|
||||
// inheriting (succeededFrom = the old key's hash, blocks above what w5-new mined alone) and w5-old with no weight
|
||||
// (succeededTo = the new key's hash); finality keeps locking with the new key signing;
|
||||
// 3. refused twice: w5-old to w5-third is refused on node 2 AND on node 0 (A has handed over); w5-new to w5-old is
|
||||
// refused (the successor named has handed over);
|
||||
// 4. the scheme byte: a vote stamped with scheme 1 is refused by every node with the reason (probe-scheme).
|
||||
// The known-failed case is run first: `--expect no-succession` with the succession never submitted must report FAIL
|
||||
// on the carried-once checks (the harness is trusted only after it fires on a failure).
|
||||
//
|
||||
// node infra/fast-time/key-succession.mjs [--expect succession|no-succession] [--secs 720] [--succeed-at-daa 260]
|
||||
// IGNEUMD, IGNEUM_MINER name the binaries (defaults: the genesis-forward fork worktree's target-remote/release on the
|
||||
// box, target/release on the Mac).
|
||||
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
||||
import { Rpc } from '../../tools/finality-attacks/lib/rpc.mjs';
|
||||
|
||||
const ROOT = new URL('../../', import.meta.url).pathname;
|
||||
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
|
||||
const BIN = process.env.IGNEUM_GF_BIN || (existsSync(`${ROOT}vendor/igneum-node-gf/target/release/igneumd`) ? `${ROOT}vendor/igneum-node-gf/target/release` : `${ROOT}vendor/igneum-node-gf/target-remote/release`);
|
||||
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
||||
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
||||
const TMP = process.env.IGNEUM_FAST_TIME_DIR || '/tmp/igneum-fast-time-w5';
|
||||
const BASE = 29720, SUFFIX = 972;
|
||||
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; };
|
||||
const GENESIS_BITS = flag('genesis-bits', 0x1f010000);
|
||||
const SECS = flag('secs', 720);
|
||||
const SUCCEED_AT = flag('succeed-at-daa', 260);
|
||||
const EXPECT = sflag('expect') || 'succession';
|
||||
if (!['succession', 'no-succession'].includes(EXPECT)) { console.error('usage: --expect succession|no-succession'); process.exit(2); }
|
||||
const started = [];
|
||||
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
for (const b of [IGNEUMD, CPU_MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
||||
|
||||
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
||||
const baseText = readFileSync(FILE, 'utf8');
|
||||
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
|
||||
const DAY_MS = field('pow_day_ms');
|
||||
const WINDOW = (() => { const m = /"weight_window":\s*([0-9]+)/.exec(baseText); return m ? +m[1] : 120; })();
|
||||
export function mergeOverrideText(text, fields) {
|
||||
let out = text;
|
||||
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
|
||||
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
|
||||
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
|
||||
}
|
||||
const override = `${TMP}/override.json`;
|
||||
writeFileSync(override, mergeOverrideText(baseText, {
|
||||
genesis_bits: GENESIS_BITS, skip_proof_of_work: false, finality_v3_activation_daa: '0', finality_leave_activation_daa: '0',
|
||||
finality_succession_activation_daa: '0', sig_scheme_activation_daa: '0', sig_scheme: '0', latency_ladder_activation_daa: NEVER,
|
||||
}));
|
||||
log(`expect ${EXPECT}; weight window ${WINDOW} DAA; the succession at DAA ${SUCCEED_AT}; run ${SECS} s`);
|
||||
|
||||
class Node {
|
||||
constructor(i, connect = []) {
|
||||
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
|
||||
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
|
||||
}
|
||||
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
|
||||
async start() {
|
||||
mkdirSync(this.dir, { recursive: true });
|
||||
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex',
|
||||
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`,
|
||||
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
|
||||
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
|
||||
const out = openSync(this.logFile, 'a');
|
||||
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] });
|
||||
started.push(this.proc);
|
||||
await sleep(1500);
|
||||
this.rpc = new Rpc(`ws://127.0.0.1:${this.jsonPort}`);
|
||||
if (!(await this.rpc.connect())) throw new Error(`n${this.i}: rpc did not open`);
|
||||
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 []; } }
|
||||
async weights() { try { return await this.rpc.call('getFinalityWeights'); } catch (e) { return null; } }
|
||||
async daa() { try { return +(await this.rpc.call('getBlockDagInfo')).virtualDaaScore; } catch { return null; } }
|
||||
}
|
||||
function miner(argv, name, env = {}) {
|
||||
const out = openSync(`${TMP}/${name}.log`, 'a');
|
||||
const p = spawn(CPU_MINER, argv, { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_POW_DAY_MS: String(DAY_MS), ...env } });
|
||||
started.push(p);
|
||||
return p;
|
||||
}
|
||||
function minerArgs(node, label, noLeave) {
|
||||
const a = ['mine', node.grpc, '1', String(SECS), label, '--engine', 'igneum-pow', '--payout-label', label, '--status-secs', '30'];
|
||||
if (noLeave) a.push('--no-leave');
|
||||
return a;
|
||||
}
|
||||
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); });
|
||||
const minerLog = (name) => { try { return readFileSync(`${TMP}/${name}.log`, 'utf8').split('\n'); } catch { return []; } };
|
||||
const keyHash = (label) => (spawnSync(CPU_MINER, ['key-hash', label], { encoding: 'utf8' }).stdout || '').trim();
|
||||
const CARRIED_LINE = /hands its weight and its forfeit term to .*(carried by|now carried by)/;
|
||||
const LOCK_LINE = /Finality: checkpoint (\d+) LOCKED/;
|
||||
|
||||
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();
|
||||
const nodes = [n0, n1, n2];
|
||||
for (const n of nodes) log(`n${n.i}: ${n.grepLog(/Key succession \(W5\) from the override file/).map(l => l.replace(/^.*?(Key succession)/, '$1'))[0] || '(no W5 line)'} | ${n.grepLog(/Signature scheme byte from the override file/).map(l => l.replace(/^.*?(Signature scheme)/, '$1'))[0] || '(no scheme line)'}`);
|
||||
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
|
||||
const OLD = 'w5-old', NEW = 'w5-new', THIRD = 'w5-third', PROBE = 'w5-probe';
|
||||
const oldHash = keyHash(OLD), newHash = keyHash(NEW);
|
||||
log(`old key ${oldHash.slice(0, 16)} (${OLD}), successor ${newHash.slice(0, 16)} (${NEW})`);
|
||||
miner(minerArgs(n0, 'w5-a0', false), 'cpu0');
|
||||
miner(minerArgs(n1, 'w5-b1', false), 'cpu1');
|
||||
let cpu2 = miner(minerArgs(n2, OLD, true), 'cpu2');
|
||||
|
||||
const samples = [];
|
||||
let phase = 'fill', oldBlocksBefore = null, succeedOutcome = null, succeededAtDaa = null, newStartedAt = null, carriedSeenAt = null;
|
||||
let refusals = [], probes = [], locksBefore = null, locksAfter = null, lastReport = 0;
|
||||
const run = (argv) => { const r = spawnSync(CPU_MINER, argv, { encoding: 'utf8' }); return { code: r.status, out: (r.stdout || '') + (r.stderr || '') }; };
|
||||
while (Date.now() - t0 < SECS * 1000) {
|
||||
await sleep(1000);
|
||||
const daa = await n0.daa();
|
||||
if (daa == null) continue;
|
||||
if (Date.now() - lastReport > 15000) {
|
||||
lastReport = Date.now();
|
||||
const w = await Promise.all(nodes.map(n => n.weights()));
|
||||
const rows = w.map(r => r ? `${r.totalWeight}/${r.voters}` : '?');
|
||||
const locks = nodes.map(n => n.grepLog(LOCK_LINE).length);
|
||||
log(`t=${since()} s daa ${daa} phase ${phase} total/voters per node ${rows.join(' ')} locks ${locks.join(' ')}`);
|
||||
samples.push({ t: +since(), daa, phase, weights: rows, locks });
|
||||
}
|
||||
if (phase === 'fill' && daa >= SUCCEED_AT) {
|
||||
const w = await n0.weights();
|
||||
const row = w?.keys?.find(k => k.keyHash === oldHash);
|
||||
oldBlocksBefore = row ? +row.blocks : 0;
|
||||
locksBefore = n0.grepLog(LOCK_LINE).length;
|
||||
log(`window filled: ${OLD} holds ${oldBlocksBefore} blocks on n0 at daa ${daa}; ${locksBefore} locks on n0`);
|
||||
try { cpu2.kill('SIGINT'); } catch { }
|
||||
await sleep(2500);
|
||||
if (EXPECT === 'succession') {
|
||||
succeedOutcome = run(['succeed', n2.grpc, OLD, NEW]);
|
||||
log(`succeed ${OLD} -> ${NEW} on n2: exit ${succeedOutcome.code}: ${succeedOutcome.out.trim().split('\n').pop()}`);
|
||||
} else {
|
||||
log(`known-failed case: no succession submitted`);
|
||||
}
|
||||
succeededAtDaa = daa;
|
||||
newStartedAt = daa;
|
||||
cpu2 = miner(minerArgs(n2, NEW, false), 'cpu2b');
|
||||
phase = 'carry';
|
||||
continue;
|
||||
}
|
||||
if (phase === 'carry') {
|
||||
const carried = nodes.map(n => n.grepLog(CARRIED_LINE).length > 0);
|
||||
if (carried.every(Boolean) && carriedSeenAt == null) { carriedSeenAt = daa; log(`carried: every node logs the succession carried by a block at daa ${daa}`); }
|
||||
// the fold: every node's report shows the successor inheriting and the old key handed over
|
||||
const w = await Promise.all(nodes.map(n => n.weights()));
|
||||
const folded = w.every(r => r && r.keys.some(k => k.keyHash === newHash && k.succeededFrom === oldHash) && r.keys.some(k => k.keyHash === oldHash && k.succeededTo === newHash));
|
||||
if (folded || (EXPECT === 'no-succession' && daa >= succeededAtDaa + 2 * WINDOW)) {
|
||||
const minedAlone = minerLog('cpu2b').filter(l => /ACCEPTED block/.test(l)).length;
|
||||
const rows = w.map(r => ({ new: r?.keys?.find(k => k.keyHash === newHash) || null, old: r?.keys?.find(k => k.keyHash === oldHash) || null }));
|
||||
log(`fold at daa ${daa}: ${NEW} mined ${minedAlone} blocks alone; per node new/old blocks ${rows.map(r => `${r.new?.blocks ?? '-'}/${r.old?.blocks ?? '-'}`).join(' ')}`);
|
||||
// refused twice, on n2 and on n0 (every node); and the successor named has handed over
|
||||
refusals = [
|
||||
{ what: `${OLD} -> ${THIRD} on n2`, ...run(['succeed', n2.grpc, OLD, THIRD]) },
|
||||
{ what: `${OLD} -> ${THIRD} on n0`, ...run(['succeed', n0.grpc, OLD, THIRD]) },
|
||||
{ what: `${NEW} -> ${OLD} on n1`, ...run(['succeed', n1.grpc, NEW, OLD]) },
|
||||
];
|
||||
for (const r of refusals) log(`${r.what}: exit ${r.code}: ${r.out.trim().split('\n').pop()}`);
|
||||
probes = nodes.map(n => ({ node: n.i, ...run(['probe-scheme', n.grpc, PROBE, '1']) }));
|
||||
for (const p of probes) log(`probe scheme 1 on n${p.node}: exit ${p.code}: ${p.out.trim().split('\n').pop()}`);
|
||||
phase = 'after';
|
||||
locksAfter = n0.grepLog(LOCK_LINE).length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (phase === 'after') {
|
||||
// locks keep coming with the new key signing: two windows after the fold is enough to see it
|
||||
const locksNow = n0.grepLog(LOCK_LINE).length;
|
||||
if (locksNow > locksAfter + 2) { log(`locks after the fold: ${locksNow} on n0 (${locksAfter} at the fold)`); break; }
|
||||
}
|
||||
}
|
||||
await sleep(1500);
|
||||
const w = await Promise.all(nodes.map(n => n.weights()));
|
||||
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return { error: e.message }; } }));
|
||||
const minedAlone = minerLog('cpu2b').filter(l => /ACCEPTED block/.test(l)).length;
|
||||
const rows = w.map(r => ({ new: r?.keys?.find(k => k.keyHash === newHash) || null, old: r?.keys?.find(k => k.keyHash === oldHash) || null }));
|
||||
const locks = nodes.map(n => n.grepLog(LOCK_LINE).length);
|
||||
const rejectedNode = nodes.map(n => n.grepLog(/PoW rejected|Rejected block|rejected block/i).length);
|
||||
const checks = {
|
||||
window_filled_before_the_succession: oldBlocksBefore != null && oldBlocksBefore >= 5,
|
||||
succession_accepted_on_submit: EXPECT === 'succession' ? succeedOutcome?.code === 0 && /SUCCEED old=/.test(succeedOutcome.out) : succeedOutcome == null,
|
||||
carried_once_on_every_node: nodes.every(n => n.grepLog(CARRIED_LINE).length >= 1),
|
||||
successor_inherits_on_every_node: rows.every(r => r.new && r.new.succeededFrom === oldHash && +r.new.blocks > minedAlone),
|
||||
old_key_handed_over_on_every_node: rows.every(r => r.old && r.old.succeededTo === newHash && +r.old.blocks === 0 && r.old.voter === false),
|
||||
nodes_agree_on_the_successor_weight: new Set(rows.map(r => String(r.new?.blocks))).size === 1,
|
||||
refused_old_again_on_n2: refusals[0]?.code === 3 && /already handed/.test(refusals[0].out),
|
||||
refused_old_again_on_n0: refusals[1]?.code === 3 && /already handed/.test(refusals[1].out),
|
||||
refused_successor_that_handed_over: refusals[2]?.code === 3 && /has itself handed/.test(refusals[2].out),
|
||||
scheme_1_refused_by_every_node: probes.length === 3 && probes.every(p => p.code === 0 && /SCHEME 1 REFUSED/.test(p.out) && /active scheme is 0/.test(p.out)),
|
||||
locks_continue_after_the_fold: locksAfter != null && locks[0] > locksAfter + 2,
|
||||
zero_rejected_by_nodes: rejectedNode.every(c => c === 0),
|
||||
sinks_agree: new Set(dag.map(d => String(d.sink))).size === 1,
|
||||
};
|
||||
const pass = Object.values(checks).every(Boolean);
|
||||
const summary = {
|
||||
expect: EXPECT, pass, checks, old_key: oldHash, new_key: newHash, old_blocks_before: oldBlocksBefore, succeeded_at_daa: succeededAtDaa,
|
||||
carried_seen_at_daa: carriedSeenAt, new_mined_alone: minedAlone, rows, locks, locks_at_fold: locksAfter, refusals: refusals.map(r => ({ what: r.what, code: r.code, line: r.out.trim().split('\n').pop() })),
|
||||
probes: probes.map(p => ({ node: p.node, code: p.code, line: p.out.trim().split('\n').pop() })), samples, wall_s: +since(),
|
||||
binaries: { igneumd: IGNEUMD, miner: CPU_MINER },
|
||||
};
|
||||
// the summary carries key hashes as their first 16 hex characters (tools/ci/no-secrets-check.sh reads committed summaries)
|
||||
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2).replace(/\b([0-9a-f]{16})[0-9a-f]{48}\b/g, '$1'));
|
||||
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'} (${EXPECT}): ${Object.entries(checks).filter(([, v]) => !v).map(([k]) => `FAILED CHECK ${k}`).join('; ') || 'every check holds'}; ${TMP}/summary.json`);
|
||||
await stopAll();
|
||||
process.exit(pass ? 0 : 1);
|
||||
|
|
@ -67,5 +67,26 @@
|
|||
"proving_v1_unproven_daa": 10,
|
||||
"proving_v1_aggregator_share_bps": 1000,
|
||||
"fees_v1_activation_daa": 0,
|
||||
"difficulty_v3_activation_daa": 18446744073709551615,
|
||||
"finality_daa_rule_activation_daa": 18446744073709551615,
|
||||
"fork_gate_activation_daa": 18446744073709551615,
|
||||
"fork_gate_window_daa": 600,
|
||||
"vote_or_burn_activation_daa": 18446744073709551615,
|
||||
"vote_burn_bps": 2000,
|
||||
"signing_bonus_activation_daa": 18446744073709551615,
|
||||
"signing_bonus_bps": 1000,
|
||||
"finality_leave_activation_daa": 18446744073709551615,
|
||||
"latency_ladder": [{"reps": 27, "admissible": true}, {"reps": 35, "admissible": true}, {"reps": 53, "admissible": true}, {"reps": 88, "admissible": false}, {"reps": 173, "admissible": false}, {"reps": 267, "admissible": false}],
|
||||
"latency_ladder_activation_daa": 18446744073709551615,
|
||||
"latency_ladder_window_daa": 120,
|
||||
"exec_restart_state_root": "",
|
||||
"emission": {"launch_rate": "3168808781", "ramp_seconds": 2592000, "ramp_start_percent": 10, "step_seconds": 63115200, "step_decay_q32": 2147483648, "tail": {"kind": "cap"}},
|
||||
"proving_consensus_verify_daa": 18446744073709551615,
|
||||
"proving_shard_program_id": "",
|
||||
"proving_aggregator_id": "",
|
||||
"sig_scheme": 0,
|
||||
"sig_scheme_activation_daa": 18446744073709551615,
|
||||
"finality_succession_activation_daa": 18446744073709551615,
|
||||
"latency_ladder_cache_rung": {"mib": 512, "admissible": false},
|
||||
"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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
a:focus-visible,button:focus-visible,input:focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px}
|
||||
.mono{font-family:var(--f-mono)}
|
||||
h1,h2{font-family:var(--f-display);margin:0;line-height:1.08;text-wrap:balance}
|
||||
.head{padding:65px 0 45px;display:flex;flex-direction:column;gap:14px}
|
||||
.head{padding:65px 0 45px;display:flex;flex-direction:column;gap:14px}.head h1{overflow-wrap:anywhere;min-width:0}
|
||||
.head p{color:var(--ink-2);max-width:67ch;font-size:18px;line-height:1.65}
|
||||
.search{display:flex;gap:10px;max-width:760px;margin-top:4px;align-items:center;padding:12px 16px;border:1px solid var(--line-2);border-radius:8px;background:var(--row)}
|
||||
.search input{flex:1;min-width:0;border:0;outline:none;background:transparent;color:var(--bone);font-size:14px}
|
||||
|
|
|
|||
|
|
@ -254,14 +254,14 @@
|
|||
<article class="record"><div class="record-top"><time datetime="2026-10-05">5 Oct 2026</time><span class="badge">log</span></div><h3>Live devnet: real transactions, the first non-empty shard proven and…</h3><p>Live devnet: real transactions, the first non-empty shard proven and paid, and the exporter's block structure fixed</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-05">5 Oct 2026</time><span class="badge">log</span></div><h3>The program id split</h3><p>The program id split: why the Apple M5 Max rejected the RTX 5090 Windows rig's proofs, and the verifier at 114 s</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-05">5 Oct 2026</time><span class="badge">log</span></div><h3>Live devnet: the first shards proven, verified and paid</h3></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>Economy simulation: mining versus proving under stress</h3><p>Sim/economy: mining versus proving under stress, agent-based</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>Difficulty rule attacked seven ways</h3><p>Difficulty rule under attack: pool hopping, pulsed rental, timestamp stretching, short-lane oscillation, epoch games, polluted window, block flood</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>Timestamp attack on the difficulty rule fixed</h3><p>Difficulty rule: timestamp attack fixed , simulator regression, 3-node forger test</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>Devnet v4: nine branches merged into one node</h3><p>Devnet-v4 integration: nine branches merged, 3-node test network on the merged node, Windows cross-build</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>Generator v2 adopted: every hash does 128 distinct reads</h3><p>Generator version 2 adopted: exact load count, fresh-source loads, program acceptance; every vector re-cut, three workers re-checked, 20,000-program census, devnet-v4 binaries rebuilt</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>First GPU proof of an Igneum block: 1.4 s on an RTX 5090</h3><p>Proving v0 on the RTX 5090: first GPU proof of an Igneum block</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>Devnet v4 live: generator v2, two-thirds floor, fresh chain</h3><p>Devnet v4 cut-over: generator v2, 2/3 floor, three nodes and a seed on a fresh chain</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>First live hourly swap: no pause on Mac, NVIDIA or AMD</h3><p>First hourly program swap on the live devnet: compile-ahead, no pause, two cards</p></article></div>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>One-click Windows workers</h3><p>One-click Windows workers: what the Apple M5 Max could measure</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>Proving: devnet v4 shards on the Apple M5 Max CPU, loaded machine</h3></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>First live finality lock: 77.4% of weight, 17 voters</h3><p>First finality lock on the live devnet: checkpoint 242 at 77.4% of all weight, two hours after genesis</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>The gfx1036 worker fault and what the Apple M5 Max could and could…</h3><p>The gfx1036 worker fault and what the Apple M5 Max could and could not reproduce</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>A node 60 s behind the clock is silently dead</h3></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>First machine on the one-click app: a 5090 at 118 MH/s</h3><p>First machine on the Igneum Miner app: the RTX 5090 Windows rig's RTX 5090 at 118 MH/s, via Setup.exe</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>Difficulty rule v2</h3><p>Difficulty rule v2: the live oscillation, its cause, the DAG replay, the fix behind a height switch</p></article>
|
||||
<article class="record"><div class="record-top"><time datetime="2026-10-04">4 Oct 2026</time><span class="badge">log</span></div><h3>The observer stored nothing for 78 minutes, then 7,022 blocks in two…</h3><p>The observer stored nothing for 78 minutes, then 7,022 blocks in two minutes</p></article></div>
|
||||
<p class="empty-state" data-filter-empty="log" hidden>Nothing in the log matches that.</p>
|
||||
<p class="note">Every entry is a dated heading of <a href="/bench">the engineering log</a>, where the commands and the hardware are. The phases and their gates are the litepaper’s <a href="/litepaper#roadmap">roadmap</a>.</p>
|
||||
<div class="onward"><a href="/bench" class="btn">The engineering log</a><a href="/evidence" class="btn">Every claim and its status</a><a href="/live" class="btn">The live devnet</a></div>
|
||||
|
|
|
|||
|
|
@ -212,43 +212,43 @@
|
|||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Sim/economy: mining versus proving under stress, agent-based",
|
||||
"short": "Economy simulation: mining versus proving under stress"
|
||||
"text": "One-click Windows workers: what the Apple M5 Max could measure",
|
||||
"short": "One-click Windows workers"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Difficulty rule under attack: pool hopping, pulsed rental, timestamp stretching, short-lane oscillation, epoch games, polluted window, block flood",
|
||||
"short": "Difficulty rule attacked seven ways"
|
||||
"text": "Proving: devnet v4 shards on the Apple M5 Max CPU, loaded machine",
|
||||
"short": "Proving: devnet v4 shards on the Apple M5 Max CPU, loaded machine"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Difficulty rule: timestamp attack fixed , simulator regression, 3-node forger test",
|
||||
"short": "Timestamp attack on the difficulty rule fixed"
|
||||
"text": "First finality lock on the live devnet: checkpoint 242 at 77.4% of all weight, two hours after genesis",
|
||||
"short": "First live finality lock: 77.4% of weight, 17 voters"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Devnet-v4 integration: nine branches merged, 3-node test network on the merged node, Windows cross-build",
|
||||
"short": "Devnet v4: nine branches merged into one node"
|
||||
"text": "The gfx1036 worker fault and what the Apple M5 Max could and could not reproduce",
|
||||
"short": "The gfx1036 worker fault and what the Apple M5 Max could and could…"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Generator version 2 adopted: exact load count, fresh-source loads, program acceptance; every vector re-cut, three workers re-checked, 20,000-program census, devnet-v4 binaries rebuilt",
|
||||
"short": "Generator v2 adopted: every hash does 128 distinct reads"
|
||||
"text": "A node 60 s behind the clock is silently dead",
|
||||
"short": "A node 60 s behind the clock is silently dead"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Proving v0 on the RTX 5090: first GPU proof of an Igneum block",
|
||||
"short": "First GPU proof of an Igneum block: 1.4 s on an RTX 5090"
|
||||
"text": "First machine on the Igneum Miner app: the RTX 5090 Windows rig's RTX 5090 at 118 MH/s, via Setup.exe",
|
||||
"short": "First machine on the one-click app: a 5090 at 118 MH/s"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "Devnet v4 cut-over: generator v2, 2/3 floor, three nodes and a seed on a fresh chain",
|
||||
"short": "Devnet v4 live: generator v2, two-thirds floor, fresh chain"
|
||||
"text": "Difficulty rule v2: the live oscillation, its cause, the DAG replay, the fix behind a height switch",
|
||||
"short": "Difficulty rule v2"
|
||||
},
|
||||
{
|
||||
"date": "2026-10-04",
|
||||
"text": "First hourly program swap on the live devnet: compile-ahead, no pause, two cards",
|
||||
"short": "First live hourly swap: no pause on Mac, NVIDIA or AMD"
|
||||
"text": "The observer stored nothing for 78 minutes, then 7,022 blocks in two minutes",
|
||||
"short": "The observer stored nothing for 78 minutes, then 7,022 blocks in two…"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
<!-- head:end -->
|
||||
<style>
|
||||
/* ledger page block (7 Oct 2026, the redesign: the page hero, the count table, the entries as records); tokens, type and components are in /site.css */
|
||||
.entry p,.entry li,.entry .status{overflow-wrap:anywhere}.entry code{white-space:normal;overflow-wrap:anywhere}
|
||||
.entry p,.entry li,.entry .status{overflow-wrap:anywhere}.status .badge{white-space:normal;max-width:100%}.entry code{white-space:normal;overflow-wrap:anywhere}
|
||||
main h2{font-size:27px;margin:64px 0 22px;padding-top:0;border-top:0;scroll-margin-top:120px}@media(max-width:560px){main h2{font-size:22px}}
|
||||
h3{font-family:var(--f-sans);font-weight:600;font-size:17px;margin:0;flex:1 1 auto;min-width:0}
|
||||
.intro{font-size:17px;color:var(--ink-2);max-width:76ch;margin:0 0 24px}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
@media (max-width:1000px){.gcol{border-right:0;border-bottom:var(--hair)}}
|
||||
.gcol .state{position:absolute;left:25px;top:14px;z-index:3;font-size:10px}
|
||||
.scene{position:relative}
|
||||
#dag{display:block;width:100%;height:418px;background:var(--row);touch-action:pan-y}
|
||||
#dag{display:block;width:100%;background:var(--row);touch-action:pan-y}
|
||||
.gtop{display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;padding:10px 25px;border-bottom:var(--hair);font:10px/1.7 var(--mono);color:var(--ash)}
|
||||
.gtop .hint{white-space:nowrap}
|
||||
.gtop .lg{display:flex;flex-wrap:wrap;gap:6px 10px;flex:1 1 auto;justify-content:center}
|
||||
|
|
@ -626,10 +626,6 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
function uninspect(){sel=null;$('insp-close').hidden=true;$('i-kind').textContent='select a block';$('i-title').textContent='Select a block';$('i-hash').textContent='Click the graph, or a row of the table below.';$('i-shard-count').textContent='not set';$('i-track').innerHTML='';$('i-shards').innerHTML='<li class="muted">No block selected.</li>';['i-incl','i-miner','i-blue','i-daa','i-parents','i-time','i-cp','i-lock'].forEach(function(id){$(id).textContent='not set';});if(proof)proof.setBlock(null);}
|
||||
$('insp-close').addEventListener('click',function(){if(dag)dag.select(null,'clear');uninspect();});
|
||||
// the four stats above the graph, from the module's own window
|
||||
// the scene box fits its lanes: the renderer places lanes at 46 px (40 on phones) under a 58 px top band and over a 38 px axis
|
||||
// (scene/live-dag.js 2.0.3 layout()); one lane per key in view plus the others lane, capped at the renderer's 7 (4 on phones)
|
||||
var fitH=0;
|
||||
function fit(st){var c=$('dag');if(!c||!st)return;var cap=st.narrow?4:7,laneH=st.narrow?40:46,keys=Math.max(2,st.minerKeys||0),lanes=Math.min(cap,keys>cap?cap:keys+(keys>cap?1:0));var h=58+lanes*laneH+38;if(h!==fitH){fitH=h;c.style.height=h+'px';}}
|
||||
function miner(st){if(!dag)return;var cps=dag.getCheckpoints(),cp=cps.length?cps[cps.length-1]:null;
|
||||
$('m-blocks').textContent=fmtN(st.blocks);$('m-win').textContent='last '+st.window+' s';$('m-keys').textContent=st.minerKeys+' miner keys · not a card count';$('lane-caption').textContent=st.minerKeys+' keys';
|
||||
$('m-mine').textContent=mine?fmtN(st.own):'0';$('m-mine').classList.toggle('dim',!mine);$('m-key').textContent=mine||'not set';
|
||||
|
|
@ -642,7 +638,7 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
onState:function(st,reason){var el=$('dag-state');el.className='state '+(st==='live'?'live':st==='failed'?'bad':'');el.innerHTML='<span class="dot '+(st==='live'?'live':st==='failed'?'bad':'off')+'"></span>'+(st==='live'?'live devnet':esc(reason||st));if(proof)proof.setPaused(st!=='live'||dag.getStats().paused);},
|
||||
onPause:function(p){$('dag-follow').classList.toggle('on',!p);if(proof)proof.setPaused(p||dag.getState().state!=='live');},
|
||||
onFollow:function(f){$('dag-follow').classList.toggle('on',f);},
|
||||
onStats:function(st){miner(st);fit(st);},
|
||||
onStats:miner,autoHeight:true,
|
||||
onSelect:function(b,why){if(b){var full=lastD&&(lastD.blocks||[]).filter(function(x){return x.hash===b.hash;})[0];inspect(Object.assign({},b,full?{number:full.number}:{}));}else uninspect();}});
|
||||
if(window.IgneumProof&&!proof)proof=IgneumProof.mount($('proof'));
|
||||
if(lastD)feed(lastD);}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<title>Igneum light client test</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<style>body{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:#0C0C0E;color:#E8E4DA;padding:24px;max-width:960px}h1{font-size:16px}td{padding:4px 10px;border-bottom:1px solid #222;vertical-align:top}.ok{color:#7ED957}.bad{color:#F2541B}</style>
|
||||
<style>body{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:#0C0C0E;color:#E8E4DA;padding:24px;max-width:960px}h1{font-size:16px}table{display:block;overflow-x:auto;max-width:100%}code{overflow-wrap:anywhere}td{padding:4px 10px;border-bottom:1px solid #222;vertical-align:top}.ok{color:#7ED957}.bad{color:#F2541B}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Igneum light client, version zero: genuine and tampered</h1>
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@
|
|||
|
||||
| Check | What it fails | Since |
|
||||
|---|---|---|
|
||||
| no text overlaps (`overlap-check.mjs`) | A served page, or a miner or wallet screen (behind `IGNEUM_OVERLAP_APPS=1`), where a visible run of text is covered by another element (a pill over a caption, a label over a value, a card over its neighbour, text under the header at rest), clipped by an overflow-hidden ancestor, or past the viewport; a page that scrolls sideways. Five widths, light and dark, the home hero at rest and at each step. A fixture with one deliberate overlap of each kind must be flagged first (`--self-test`). Needs a headless Chromium: CI installs Playwright; the Mac ships the pages to build-2 (`infra/build-server/overlap-browser.sh`). | 7 October 2026: the hero's step pill sat on the caption's second line ("Two thirds of the weight sign. The checkpoint locks.") at every desktop width, found by the project lead on the live site |
|
||||
|
||||
| kill by exact command or pid file (owed as a check) | 6 October 2026, 21:09Z: a Mac-side `pkill -f <log file name>` matched nothing (the log name was a redirect, not part of the command line), the roll-everything script lived on and wiped a box it had been told to hold. Rule: a job is stopped by its pid file (`tools/fleet/fleet-bg.sh start|stop <name>`) or by a pattern anchored on its exact command line (`^python3 -u /root/fleet/in/box-prover.py`), never by a word that may or may not appear in it. The check that flags a `pkill -f`/`pgrep -f` whose literal is a path or a name that never starts a command line is owed to the CI lane |
|
||||
|
|
|
|||
|
|
@ -6,9 +6,14 @@
|
|||
# infra/build-server/remote-run.sh --self-test-slots (slots, jobs, quiet beside builds and suites, leased cores excluded).
|
||||
#
|
||||
# tools/ci/box-locks-check.sh # exit 1 when either self-test fails or the box cannot be reached
|
||||
# tools/ci/box-locks-check.sh --ci # the same, but a runner with no box (no BUILD_HOST, no ~/.config/igneum/build-server) prints
|
||||
# # "ok (no box from this runner)" and exits 0: GitHub's hosted runner cannot reach the box
|
||||
# # (master went red on 7 Oct 2026 the moment this check landed); the Mac hook and the
|
||||
# # self-hosted runners that carry the box file run it live
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../.."
|
||||
. infra/build-server/lib.sh
|
||||
if [ "${1:-}" = --ci ] && [ -z "${BUILD_HOST:-}" ] && [ ! -s "$BS_HOST_FILE" ]; then echo "box-locks: ok (no box from this runner: no BUILD_HOST and no $BS_HOST_FILE; the Mac hook and the box runners run the lock self-tests)"; exit 0; fi
|
||||
bs_host 1
|
||||
stamp="ci-$$-$(date +%s)"
|
||||
scp -q "${BS_SSH_OPTS[@]}" infra/build-server/lease.sh "$BS_HOST:/tmp/lease-$stamp.sh"
|
||||
|
|
|
|||
443
tools/ci/overlap-check.mjs
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
#!/usr/bin/env node
|
||||
// The text-overlap sweep: renders every served page of the site, and the miner and wallet UIs through their mocks, in a
|
||||
// headless Chromium at five widths in light and dark (the home hero at rest and at each of its three steps), reads every
|
||||
// visible run of text and flags what a reader cannot read: text COVERED by another element (a pill over a caption, a label
|
||||
// over a value, a card over its neighbour, text under the header), text CLIPPED by an overflow-hidden ancestor, and text
|
||||
// running OUTSIDE the viewport. The owner found the first one on 7 October 2026: the home hero's step pill sat on the
|
||||
// caption's second line ("Two thirds of the weight sign. The checkpoint locks.") at every desktop width.
|
||||
//
|
||||
// node tools/ci/overlap-check.mjs --self-test a fixture with one deliberate overlap of each kind is flagged, a
|
||||
// clean one passes (the known-failed-first rule; runs before any sweep)
|
||||
// node tools/ci/overlap-check.mjs --site [dir] the site tree (default site/), every *.html, the rewrites, the hero steps
|
||||
// node tools/ci/overlap-check.mjs --apps [tree] the miner UI (tools/ui-mock/server.mjs) and the wallet UI
|
||||
// (tools/ui-mock/wallet.mjs) from a repository tree (default this one);
|
||||
// a UI directory absent from the tree is reported and skipped
|
||||
// node tools/ci/overlap-check.mjs --previews <dir> the owner's preview set: twelve app screens, dark, 1280 and 900 wide, PNG
|
||||
// options: --widths 390,768,1024,1280,1600 --themes light,dark --shots <dir> (a PNG per render) --json <file> (the findings)
|
||||
// --jobs 4 (parallel pages) --only <substring> (page names) --local (never go to the box) --box N (which box)
|
||||
//
|
||||
// Where it runs: a browser is needed. The script resolves Playwright from this tree's node_modules, $IGNEUM_PLAYWRIGHT_DIR
|
||||
// or /srv/builds/_bin/overlap (the box install, infra/build-server/overlap-browser.sh). Without one, and unless --local,
|
||||
// it ships its inputs to a build box (rsync into /srv/builds/_overlap/<worktree>/, nothing heavy on the Mac) and runs
|
||||
// there under nice 10, shots and JSON coming back; CI installs Playwright in the workflow and runs it in place.
|
||||
//
|
||||
// How a cover is decided: for every run of text, five points along its visible part are hit-tested (elementFromPoint,
|
||||
// with pointer-events forced on so decorative captions are hit too); a point whose top element is neither the text's
|
||||
// element, nor inside it, nor one of its ancestors is covered when something between that element and the common
|
||||
// ancestor paints (a background, a border, an image, a canvas, or text of its own). A transparent wrapper over text is
|
||||
// not a cover. A dialog or a toast that is MEANT to sit over the page (role=dialog, aria-modal, the update card, a toast)
|
||||
// is a cover by design and is not flagged; the ALLOW table below carries each with its reason. Animations and
|
||||
// transitions are frozen at their end state and smooth scrolling is off before measuring, so a reveal in flight is not a
|
||||
// finding. Clipping by an ancestor with text-overflow: ellipsis is reported as "truncated", a note, not a failure.
|
||||
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { join, dirname, resolve, extname, basename } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import http from 'node:http';
|
||||
import net from 'node:net';
|
||||
import os from 'node:os';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(here, '..', '..');
|
||||
const args = process.argv.slice(2);
|
||||
const opt = (name, dflt) => { const i = args.indexOf(name); return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true) : dflt; };
|
||||
const has = (name) => args.includes(name);
|
||||
const WIDTHS = String(opt('--widths', '390,768,1024,1280,1600')).split(',').map(Number);
|
||||
const THEMES = String(opt('--themes', 'light,dark')).split(',');
|
||||
const JOBS = Number(opt('--jobs', 4));
|
||||
const ONLY = opt('--only', '');
|
||||
const SHOTS = opt('--shots', '');
|
||||
const JSON_OUT = opt('--json', '');
|
||||
const HEIGHT_FOR = (w, app) => app ? (w <= 900 ? 700 : 800) : (w <= 390 ? 844 : w <= 768 ? 1024 : w <= 1024 ? 768 : w <= 1280 ? 800 : 900);
|
||||
|
||||
// covers that are meant to sit over the page: a match on the cover's selector path, with the reason
|
||||
const ALLOW = [
|
||||
{ re: /\[role="?dialog"?\]|\[aria-modal|#upd\b|\.upd-card|\.update-card/, why: 'the update card is one centred card over everything (miner and wallet, by design)' },
|
||||
{ re: /\.toast\b|#toast\b/, why: 'a toast sits over the page for a few seconds' },
|
||||
{ re: /\.nav-sheet\b/, why: 'the open menu sheet covers the page on purpose' },
|
||||
{ re: /\.tip\b/, why: 'a chart tooltip follows the pointer over the chart' },
|
||||
];
|
||||
|
||||
// ---------- the in-page measurer (serialised into the page; no closure over this file) ----------
|
||||
async function measureInPage(opts) {
|
||||
const tol = opts.tol || 2, vw = innerWidth, vh = innerHeight;
|
||||
const st = document.createElement('style'); st.id = '__ov_style';
|
||||
st.textContent = '*{pointer-events:auto!important}html{scroll-behavior:auto!important}*,*::before,*::after{transition:none!important;animation-duration:0s!important;animation-delay:0s!important}';
|
||||
const skip = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'TITLE', 'OPTION', 'SELECT', 'TEXTAREA', 'HEAD', 'META', 'LINK']);
|
||||
const alpha = (c) => { if (!c || c === 'transparent') return 0; const m = /rgba?\(([^)]+)\)/.exec(c); if (!m) return 1; const p = m[1].split(/[\s,\/]+/).map(parseFloat); return p.length >= 4 ? p[3] : 1; };
|
||||
const ownText = (el) => { for (const n of el.childNodes) if (n.nodeType === 3 && n.data.trim()) return true; return false; };
|
||||
const paints = (el) => {
|
||||
if (['IMG', 'CANVAS', 'VIDEO', 'PICTURE', 'IFRAME', 'INPUT', 'SELECT', 'TEXTAREA'].includes(el.tagName)) return true;
|
||||
if (el.tagName === 'svg') return false; // an <svg> root hit with no shape under the point is an empty area; a filled shape is hit as itself
|
||||
const cs = getComputedStyle(el);
|
||||
if (cs.backgroundImage !== 'none' || alpha(cs.backgroundColor) > 0.02) return true;
|
||||
for (const side of ['Top', 'Bottom', 'Left', 'Right']) if (parseFloat(cs['border' + side + 'Width']) > 0 && alpha(cs['border' + side + 'Color']) > 0.02) return true;
|
||||
return ownText(el);
|
||||
};
|
||||
const visible = (el) => {
|
||||
if (el.checkVisibility && !el.checkVisibility({ opacityProperty: true, visibilityProperty: true, contentVisibilityAuto: true })) return false; // closed <details>, display none, content-visibility
|
||||
for (let e = el; e && e.nodeType === 1; e = e.parentElement) { if (e.hidden) return false; const cs = getComputedStyle(e); if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0 || cs.contentVisibility === 'hidden') return false; if (e.tagName === 'DETAILS' && !e.open && !(e.firstElementChild && e.firstElementChild.tagName === 'SUMMARY' && e.firstElementChild.contains(el))) return false; }
|
||||
return true;
|
||||
};
|
||||
// the author's pointer-events, read before the override below: a cover with pointer-events none and no text of its own is
|
||||
// decorative (a glow, a gradient, a vignette) and never hides text on purpose
|
||||
const noPointer = new WeakSet(); for (const e of document.querySelectorAll('*')) if (getComputedStyle(e).pointerEvents === 'none') noPointer.add(e);
|
||||
document.head.appendChild(st);
|
||||
const sel = (el) => { const parts = []; for (let e = el, i = 0; e && e !== document.body && i < 4; e = e.parentElement, i++) { let s = e.tagName.toLowerCase(); if (e.id) s += '#' + e.id; else if (e.classList.length) s += '.' + [...e.classList].slice(0, 2).join('.'); parts.unshift(s); } return parts.join(' > '); };
|
||||
const inter = (a, b) => ({ l: Math.max(a.l, b.l), t: Math.max(a.t, b.t), r: Math.min(a.r, b.r), b: Math.min(a.b, b.b) });
|
||||
const findings = [], keys = new Set();
|
||||
const add = (kind, el, by, text, q, how) => { const k = kind + '|' + sel(el) + '|' + (by ? sel(by) : '') + '|' + text.slice(0, 30); if (keys.has(k)) return; keys.add(k); findings.push({ kind, text, el: sel(el), by: by ? sel(by) : '', how, box: [Math.round(q.left), Math.round(q.top + scrollY), Math.round(q.width), Math.round(q.height)] }); };
|
||||
const docH = Math.min(document.documentElement.scrollHeight, 30000);
|
||||
let runs = 0;
|
||||
for (let y0 = 0; y0 < docH; y0 += vh) {
|
||||
const lastWindow = y0 + vh >= docH;
|
||||
scrollTo(0, y0);
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||
const visCache = new Map(), clipCache = new Map();
|
||||
const clipChain = (el) => { // the ancestors (and the element) that clip or scroll, once per element per window
|
||||
let c = clipCache.get(el); if (c) return c; c = [];
|
||||
for (let a = el; a && a !== document.documentElement; a = a.parentElement) {
|
||||
const acs = getComputedStyle(a); const ox = acs.overflowX, oy = acs.overflowY;
|
||||
const cx = ox === 'hidden' || ox === 'clip', cy = oy === 'hidden' || oy === 'clip', scrolls = /auto|scroll/.test(ox + ' ' + oy);
|
||||
if (!cx && !cy && !scrolls) continue;
|
||||
if (a.clientWidth === 0 && a.clientHeight === 0) continue;
|
||||
c.push({ a, cx, cy, scrolls, srOnly: a.clientWidth <= 1 || a.clientHeight <= 1 });
|
||||
}
|
||||
clipCache.set(el, c); return c;
|
||||
};
|
||||
let n;
|
||||
while ((n = walker.nextNode())) {
|
||||
if (!n.data.trim()) continue;
|
||||
const el = n.parentElement; if (!el || skip.has(el.tagName)) continue;
|
||||
let v = visCache.get(el); if (v === undefined) { v = !el.closest('select,textarea') && visible(el); if (v) { const cs = getComputedStyle(el); if (parseFloat(cs.fontSize) < 1 || alpha(cs.color) < 0.05) v = false; } visCache.set(el, v); }
|
||||
if (!v) continue;
|
||||
const cs = getComputedStyle(el);
|
||||
const range = document.createRange(); range.selectNodeContents(n);
|
||||
const text = n.data.replace(/\s+/g, ' ').trim().slice(0, 70);
|
||||
for (const q of range.getClientRects()) {
|
||||
if (q.width <= 1 || q.height <= 1 || q.bottom < 0 || q.top > vh) continue;
|
||||
runs++;
|
||||
let vis = { l: q.left, t: q.top, r: q.right, b: q.bottom }, clipper = null, cut = 0, srOnly = false;
|
||||
for (const { a, cx, cy, scrolls, srOnly: sr } of clipChain(el)) {
|
||||
if (sr) { srOnly = true; break; } // the visually-hidden pattern: a 1 px box for screen readers
|
||||
const ab = a.getBoundingClientRect();
|
||||
const box = { l: ab.left + a.clientLeft, t: ab.top + a.clientTop, r: ab.left + a.clientLeft + a.clientWidth, b: ab.top + a.clientTop + a.clientHeight };
|
||||
if (scrolls && !cx && !cy) {
|
||||
// a table wrapper or a <pre> scrolls sideways by design (no vertical scrolling of its own); a pane that scrolls
|
||||
// vertically AND is wider than its box (the wallet's main, a settings page) hides text sideways
|
||||
if (a.scrollHeight > a.clientHeight + tol && a.scrollWidth > a.clientWidth + tol && (vis.r > box.r + tol || vis.l < box.l - tol)) add('outside', el, a, text, q, Math.round(vis.r > box.r + tol ? vis.r - box.r : box.l - vis.l) + ' px past the edge of ' + sel(a) + ', a pane that scrolls both ways');
|
||||
vis = inter(vis, box); continue;
|
||||
}
|
||||
const cutHere = Math.max(cx ? Math.max(box.l - vis.l, vis.r - box.r) : 0, cy ? Math.max(box.t - vis.t, vis.b - box.b) : 0);
|
||||
if (cutHere > tol && cutHere > cut) { cut = cutHere; clipper = a; }
|
||||
vis = inter(vis, { l: cx ? box.l : -1e9, t: cy ? box.t : -1e9, r: cx ? box.r : 1e9, b: cy ? box.b : 1e9 });
|
||||
}
|
||||
if (srOnly) continue;
|
||||
if (vis.r - vis.l > 2 && (vis.l < -tol || vis.r > vw + tol)) add('outside', el, null, text, q, (vis.l < -tol ? Math.round(-vis.l) + ' px past the left edge' : Math.round(vis.r - vw) + ' px past the right edge') + ' at ' + vw + ' px');
|
||||
if (clipper) {
|
||||
const ccs = getComputedStyle(clipper);
|
||||
const ellipsis = (ccs.textOverflow === 'ellipsis' || cs.textOverflow === 'ellipsis') && (ccs.whiteSpace === 'nowrap' || cs.whiteSpace === 'nowrap');
|
||||
add(ellipsis ? 'truncated' : 'clipped', el, clipper, text, q, Math.round(cut) + ' px cut by ' + sel(clipper));
|
||||
}
|
||||
if (vis.r - vis.l < 2 || vis.b - vis.t < Math.min(q.height * 0.5, 8)) continue; // a sliver at a scroll pane's edge is scrolled away, not covered (the 1 px drawer bar under the miner's Earnings page, 7 Oct 2026)
|
||||
const ym = (vis.t + vis.b) / 2; if (ym < 0 || ym > vh) continue;
|
||||
const w = vis.r - vis.l; let covered = 0, by = null, pts = 0;
|
||||
for (const f of [0.08, 0.3, 0.5, 0.7, 0.92]) {
|
||||
const x = vis.l + w * f; if (x < 0 || x > vw) continue; pts++;
|
||||
const top = document.elementFromPoint(x, ym);
|
||||
if (!top || top === el || el.contains(top) || top.contains(el)) continue;
|
||||
let p = top, painted = null, fixed = null;
|
||||
while (p && !p.contains(el)) { const pcs = getComputedStyle(p); if (!fixed && (pcs.position === 'fixed' || pcs.position === 'sticky')) fixed = p; if (!painted && paints(p) && !(noPointer.has(p) && !ownText(p) && !/^(IMG|CANVAS|VIDEO)$/.test(p.tagName))) painted = p; p = p.parentElement; }
|
||||
if (!painted) continue;
|
||||
if (fixed) { // a bar that travels with the viewport: content scrolling under it is by design; what sits under it at rest (the top, first window) or at the end (the bottom, last window) is not
|
||||
const fr = fixed.getBoundingClientRect(); const atTop = fr.top <= 2 && y0 === 0, atBottom = fr.bottom >= vh - 2 && lastWindow;
|
||||
if (!atTop && !atBottom) continue;
|
||||
}
|
||||
covered++; if (!by) by = painted;
|
||||
}
|
||||
if (covered) add('covered', el, by, text, q, covered + ' of ' + pts + ' points under ' + sel(by));
|
||||
}
|
||||
}
|
||||
}
|
||||
scrollTo(0, 0);
|
||||
const scrollW = document.documentElement.scrollWidth, wide = [];
|
||||
if (scrollW > vw + tol) { // name the outermost elements that reach past the viewport (the cause of a sideways page)
|
||||
for (const e of document.body.querySelectorAll('*')) { if (!visible(e)) continue; const r = e.getBoundingClientRect(); if (r.right > vw + tol && r.width > 2 && !(e.parentElement && e.parentElement.getBoundingClientRect().right > vw + tol && e.parentElement !== document.body)) wide.push({ el: sel(e), right: Math.round(r.right), width: Math.round(r.width) }); }
|
||||
wide.sort((a, b) => b.right - a.right);
|
||||
}
|
||||
st.remove();
|
||||
const status = document.getElementById('scene-status');
|
||||
return { findings, runs, vw, vh, docH, scrollW, wide: wide.slice(0, 4), status: status ? status.textContent : '' };
|
||||
}
|
||||
|
||||
// ---------- servers ----------
|
||||
function freePort() { return new Promise((res) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => res(p)); }); }); }
|
||||
const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.mjs': 'application/javascript; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', '.webp': 'image/webp', '.woff2': 'font/woff2', '.json': 'application/json', '.webmanifest': 'application/manifest+json', '.txt': 'text/plain', '.xml': 'application/xml' };
|
||||
function serveSite(dir) {
|
||||
// the site as Vercel serves it: cleanUrls, the two rewrites, /api/* answering nothing (the pages show their rest state)
|
||||
const rewrites = [[/^\/block\/[^/]+$/, '/block.html'], [/^\/address\/[^/]+$/, '/address.html'], [/^\/benchmarks$/, '/miners.html']];
|
||||
return new Promise((res) => {
|
||||
const srv = http.createServer((req, rs) => {
|
||||
let p = decodeURIComponent(new URL(req.url, 'http://x').pathname);
|
||||
if (p.startsWith('/api/')) { rs.writeHead(503, { 'Content-Type': 'application/json' }); return rs.end('{}'); }
|
||||
for (const [re, to] of rewrites) if (re.test(p)) p = to;
|
||||
if (p === '/') p = '/index.html';
|
||||
let f = join(dir, p);
|
||||
if (!extname(f) && existsSync(f + '.html')) f += '.html';
|
||||
if (existsSync(f) && statSync(f).isDirectory()) f = join(f, 'index.html');
|
||||
if (!f.startsWith(dir) || !existsSync(f) || statSync(f).isDirectory()) { const nf = join(dir, '404.html'); rs.writeHead(404, { 'Content-Type': 'text/html' }); return rs.end(existsSync(nf) ? readFileSync(nf) : 'not found'); }
|
||||
rs.writeHead(200, { 'Content-Type': MIME[extname(f)] || 'application/octet-stream', 'Cache-Control': 'no-store' }); rs.end(readFileSync(f));
|
||||
});
|
||||
srv.listen(0, '127.0.0.1', () => res({ port: srv.address().port, close: () => srv.close() }));
|
||||
});
|
||||
}
|
||||
async function spawnMock(script, uiDir) {
|
||||
const port = await freePort();
|
||||
const child = spawn(process.execPath, [script, String(port)].concat(uiDir ? [uiDir] : []), { stdio: ['ignore', 'ignore', 'inherit'] });
|
||||
for (let i = 0; i < 50; i++) { await new Promise((r) => setTimeout(r, 100)); const ok = await new Promise((r) => { const s = net.connect(port, '127.0.0.1'); s.on('connect', () => { s.destroy(); r(true); }); s.on('error', () => r(false)); }); if (ok) break; }
|
||||
return { port, close: () => child.kill() };
|
||||
}
|
||||
|
||||
// ---------- targets ----------
|
||||
function siteTargets(dir, port) {
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const pages = readdirSync(dir).filter((f) => f.endsWith('.html')).sort();
|
||||
const out = [];
|
||||
for (const f of pages) {
|
||||
const name = f.replace(/\.html$/, '');
|
||||
const path = name === 'index' ? '/' : name === '404' ? '/no-such-page' : '/' + name;
|
||||
out.push({ surface: 'site', name: name === 'index' ? 'home' : name, url: base + path, app: false });
|
||||
}
|
||||
out.push({ surface: 'site', name: 'block-id', url: base + '/block/1234', app: false }, { surface: 'site', name: 'address-id', url: base + '/address/0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', app: false });
|
||||
if (existsSync(join(dir, 'verify', 'test.html'))) out.push({ surface: 'site', name: 'verify-test', url: base + '/verify/test.html', app: false });
|
||||
// the home hero at rest and at each step: the step buttons pin the stage (and its caption) before the sweep
|
||||
const home = out.find((t) => t.name === 'home'); if (home) home.steps = [0, 1, 2];
|
||||
return out;
|
||||
}
|
||||
function minerTargets(port) {
|
||||
const u = (q) => `http://127.0.0.1:${port}/t/mock/?${q}`;
|
||||
const screens = [['setup-welcome', 'scenario=fresh&screen=welcome'], ['setup-cards', 'scenario=fresh&screen=cards'], ['setup-region', 'scenario=fresh&screen=region'], ['setup-address', 'scenario=fresh&screen=address'], ['cards-none', 'scenario=nocards&screen=cards']];
|
||||
const pages = ['overview', 'cards', 'earnings', 'prove', 'settings'];
|
||||
const scenarios = ['live', 'nvidia', 'syncing', 'nodedown', 'nocards', 'integrated', 'clock', 'paused', 'heat', 'intel', 'firstwait', 'firstblock', 'ladder', 'job', 'biglog'];
|
||||
const out = screens.map(([n, q]) => ({ surface: 'miner', name: n, url: u(q), app: true }));
|
||||
for (const s of scenarios) for (const p of pages) { if (s !== 'live' && s !== 'nvidia' && p !== 'overview' && p !== 'cards' && !(s === 'ladder' && p === 'earnings') && !(s === 'job' && p === 'settings')) continue; out.push({ surface: 'miner', name: `${s}-${p}`, url: u(`scenario=${s}&page=${p}`), app: true }); }
|
||||
for (const k of ['available', 'downloading', 'ready', 'waiting', 'applying', 'urgent', 'manual', 'error', 'updated']) out.push({ surface: 'miner', name: `update-${k}`, url: u(`scenario=live&page=overview&update=${k}&card=1`), app: true });
|
||||
out.push({ surface: 'miner', name: 'job-running', url: u('scenario=live&page=overview&job=running'), app: true }, { surface: 'miner', name: 'job-failed', url: u('scenario=live&page=overview&job=failed'), app: true });
|
||||
return out;
|
||||
}
|
||||
function walletTargets(port) {
|
||||
const u = (q) => `http://127.0.0.1:${port}/t/mock/?${q}`;
|
||||
// the scenarios of tools/ui-mock/wallet.mjs (wallet-0.1.5): welcome, unlock, home, public, scanning, nonode, plain, empty, update
|
||||
const out = [['lock', 'scenario=unlock'], ['lock-touch', 'scenario=unlock&bio=touch'], ['welcome', 'scenario=welcome'], ['create', 'scenario=welcome&screen=create'], ['import', 'scenario=welcome&screen=import'],
|
||||
['home', 'scenario=home&page=home'], ['send', 'scenario=home&page=send'], ['receive', 'scenario=home&page=receive'], ['history', 'scenario=home&page=history'], ['settings', 'scenario=home&page=settings'],
|
||||
['public-home', 'scenario=public&page=home'], ['public-settings', 'scenario=public&page=settings'], ['scanning-home', 'scenario=scanning&page=home'], ['nonode-home', 'scenario=nonode&page=home'], ['nonode-send', 'scenario=nonode&page=send'],
|
||||
['plain-send', 'scenario=plain&page=send'], ['empty-home', 'scenario=empty&page=home'], ['empty-history', 'scenario=empty&page=history'], ['update-card', 'scenario=update&page=home'], ['update-downloading', 'scenario=home&page=home&update=downloading&card=1']];
|
||||
return out.map(([n, q]) => ({ surface: 'wallet', name: n, url: u(q), app: true }));
|
||||
}
|
||||
|
||||
// ---------- the sweep ----------
|
||||
async function loadPlaywright() {
|
||||
const dirs = [root, process.env.IGNEUM_PLAYWRIGHT_DIR, '/srv/builds/_bin/overlap'].filter(Boolean);
|
||||
for (const d of dirs) { try { const req = createRequire(join(d, 'package.json')); return req('playwright'); } catch (e) { /* next */ } }
|
||||
return null;
|
||||
}
|
||||
async function renderAll(pw, targets, label) {
|
||||
const browser = await pw.chromium.launch({ args: ['--no-sandbox', '--disable-gpu', '--font-render-hinting=none'] });
|
||||
const jobs = [];
|
||||
for (const t of targets) for (const w of WIDTHS) for (const theme of THEMES) {
|
||||
if (t.app && w < 720) continue; // the apps are desktop windows; 720 px is their smallest layout
|
||||
jobs.push({ t, w, theme });
|
||||
}
|
||||
const results = []; let next = 0, done = 0; const t0 = Date.now();
|
||||
async function worker() {
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 }, deviceScaleFactor: 1, colorScheme: 'dark', reducedMotion: 'no-preference' });
|
||||
const page = await ctx.newPage();
|
||||
page.on('pageerror', () => {});
|
||||
let curTheme = '';
|
||||
while (next < jobs.length) {
|
||||
const j = jobs[next++]; const { t, w, theme } = j;
|
||||
if (theme !== curTheme) { await page.emulateMedia({ colorScheme: theme }); curTheme = theme; }
|
||||
await page.setViewportSize({ width: w, height: HEIGHT_FOR(w, t.app) });
|
||||
const url = t.app ? t.url + '&theme=' + theme : t.url;
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'load', timeout: 20000 });
|
||||
await page.evaluate(() => document.fonts && document.fonts.ready).catch(() => {});
|
||||
await page.waitForLoadState('networkidle', { timeout: 2500 }).catch(() => {});
|
||||
await page.waitForTimeout(t.app ? 700 : 300);
|
||||
const variants = [{ step: 'rest', act: null }].concat((t.steps || []).map((s) => ({ step: 'step' + (s + 1), act: s })));
|
||||
for (const v of variants) {
|
||||
if (v.act !== null) { const b = page.locator(`#scene-steps [data-stage="${v.act}"]`); if (await b.count()) { await b.first().click({ force: true }); await page.waitForTimeout(80); } }
|
||||
const m = await page.evaluate(measureInPage, { tol: 2 });
|
||||
const key = `${t.surface}/${t.name} ${w}px ${theme}${v.step === 'rest' ? '' : ' ' + v.step}`;
|
||||
if (SHOTS) { mkdirSync(SHOTS, { recursive: true }); await page.screenshot({ path: join(SHOTS, `${t.surface}__${t.name}__${w}__${theme}${v.step === 'rest' ? '' : '__' + v.step}.png`), fullPage: !t.app }).catch(() => {}); }
|
||||
results.push({ key, surface: t.surface, name: t.name, width: w, theme, step: v.step, status: m.status, runs: m.runs, scrollW: m.scrollW, findings: m.findings });
|
||||
}
|
||||
} catch (e) { results.push({ key: `${t.surface}/${t.name} ${w}px ${theme}`, surface: t.surface, name: t.name, width: w, theme, step: 'rest', error: String(e.message || e).split('\n')[0], findings: [] }); }
|
||||
done++;
|
||||
}
|
||||
await ctx.close();
|
||||
}
|
||||
await Promise.all(Array.from({ length: Math.min(JOBS, jobs.length) }, worker));
|
||||
await browser.close();
|
||||
return { results, secs: (Date.now() - t0) / 1000, renders: jobs.length, label };
|
||||
}
|
||||
function allowed(f) { return ALLOW.find((a) => a.re.test(f.by || '')); }
|
||||
function report(run) {
|
||||
const fails = [], notes = [], errors = [];
|
||||
for (const r of run.results) {
|
||||
if (r.error) { errors.push(`${r.key}: ${r.error}`); continue; }
|
||||
if (r.scrollW > r.width + 2) fails.push(`${r.key}: the page scrolls sideways (${r.scrollW} px wide in a ${r.width} px viewport; past the edge: ${(r.wide || []).map((w) => w.el + ' to ' + w.right + ' px').join('; ') || 'no visible element, an overflowing pseudo-element or margin'})`);
|
||||
for (const f of r.findings) {
|
||||
const a = allowed(f);
|
||||
const line = `${r.key}${r.status ? ' [' + r.status + ']' : ''}: ${f.kind.toUpperCase()} "${f.text}" in ${f.el} (${f.how})`;
|
||||
if (a) notes.push(line + ' [allowed: ' + a.why + ']'); else if (f.kind === 'truncated') notes.push(line); else fails.push(line);
|
||||
}
|
||||
}
|
||||
return { fails, notes, errors };
|
||||
}
|
||||
|
||||
// ---------- the self-test: known failed first ----------
|
||||
const FIXTURE_BAD = `<!doctype html><meta charset=utf-8><style>body{margin:0;font:14px/1.5 sans-serif;width:390px}
|
||||
.scene{position:relative;height:300px;background:#222;color:#ddd}
|
||||
.cap{position:absolute;left:12px;bottom:20px;max-width:52%}
|
||||
.pill{position:absolute;left:50%;bottom:40px;transform:translateX(-50%);background:#444;border:1px solid #777;padding:8px 12px;white-space:nowrap}
|
||||
.clip{overflow:hidden;height:21px;width:200px}.clip p{margin:0}
|
||||
.far{position:absolute;left:600px;top:10px;white-space:nowrap}
|
||||
.wrap{position:relative;height:40px}.label{position:absolute;left:0;top:0}.value{position:absolute;left:10px;top:4px}
|
||||
</style>
|
||||
<div class=scene><div class=cap><strong>03 / Locking</strong><br>Two thirds of the weight sign. The checkpoint locks. The caption runs on under the pill.</div><div class=pill>01 Mine 02 Prove 03 Lock</div></div>
|
||||
<div class=clip><p>A first line that fits</p><p>A second line cut off by overflow hidden</p></div>
|
||||
<div class=wrap><span class=label>Hash rate</span><span class=value>124 MH/s</span></div>
|
||||
<p class=far>Text pushed past the right edge of the viewport</p>`;
|
||||
const FIXTURE_GOOD = `<!doctype html><meta charset=utf-8><style>body{margin:0;font:14px/1.5 sans-serif;padding:16px;overflow-x:hidden}
|
||||
.scene{position:relative;height:300px;background:#222;color:#ddd}.cap{position:absolute;left:12px;bottom:20px;max-width:52%}
|
||||
.pill{position:absolute;left:50%;top:12px;transform:translateX(-50%);background:#444;border:1px solid #777;padding:8px 12px;white-space:nowrap}
|
||||
.card{border:1px solid #999;padding:12px;margin:12px 0}.ell{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:160px}
|
||||
.behind{position:absolute;inset:0;z-index:1}
|
||||
</style>
|
||||
<div class=scene><div class=cap><strong>03 / Locking</strong><br>Two thirds of the weight sign. The checkpoint locks.</div><div class=pill>01 Mine 02 Prove 03 Lock</div><a class=behind href=#></a></div>
|
||||
<div class=card><h2>A card</h2><p>Some words with <a href=#>a link</a> and <strong>bold</strong> inline.</p><p class=ell>A long single line that ends in an ellipsis by design</p></div>
|
||||
<button>A button</button> <span>beside text</span>
|
||||
<div style="position:fixed;left:0;right:0;bottom:0;height:120px;overflow:auto"><p style="margin:0;line-height:20px">line 1<br>line 2<br>line 3<br>line 4<br>line 5<br>line 6<br>line 7 scrolled half under the pane's edge<br>line 8<br>line 9</p></div>
|
||||
<div style="position:fixed;left:0;right:0;bottom:0;height:0;border-top:1px solid #777;box-shadow:0 -12px 30px #000"></div>`;
|
||||
async function selfTest(pw) {
|
||||
const srv = http.createServer((req, rs) => { rs.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); rs.end(req.url.includes('good') ? FIXTURE_GOOD : FIXTURE_BAD); });
|
||||
await new Promise((r) => srv.listen(0, '127.0.0.1', r)); const port = srv.address().port;
|
||||
const savedW = WIDTHS.splice(0, WIDTHS.length, 390), savedT = THEMES.splice(0, THEMES.length, 'dark');
|
||||
const run = await renderAll(pw, [{ surface: 'fixture', name: 'bad', url: `http://127.0.0.1:${port}/bad`, app: false }, { surface: 'fixture', name: 'good', url: `http://127.0.0.1:${port}/good`, app: false }], 'self-test');
|
||||
WIDTHS.splice(0, WIDTHS.length, ...savedW); THEMES.splice(0, THEMES.length, ...savedT);
|
||||
srv.close();
|
||||
const bad = run.results.find((r) => r.name === 'bad'), good = run.results.find((r) => r.name === 'good');
|
||||
const kinds = (r) => new Set(r.findings.filter((f) => !allowed(f)).map((f) => f.kind));
|
||||
const problems = [];
|
||||
if (!bad || bad.error) problems.push('the bad fixture did not render: ' + (bad && bad.error));
|
||||
else {
|
||||
const k = kinds(bad);
|
||||
if (!bad.findings.some((f) => f.kind === 'covered' && /Two thirds/.test(f.text) && /pill/.test(f.by))) problems.push('the pill over the caption was not flagged (covered)');
|
||||
if (!bad.findings.some((f) => f.kind === 'clipped' && /second line/.test(f.text))) problems.push('the line under overflow hidden was not flagged (clipped)');
|
||||
if (!bad.findings.some((f) => f.kind === 'outside' && /past the right/.test(f.text))) problems.push('the text past the viewport was not flagged (outside)');
|
||||
if (!bad.findings.some((f) => f.kind === 'covered' && /Hash rate|124 MH/.test(f.text))) problems.push('the label over the value was not flagged (covered, text over text)');
|
||||
if (!k.has('covered') || !k.has('clipped') || !k.has('outside')) problems.push('kinds found on the bad fixture: ' + [...k].join(','));
|
||||
}
|
||||
if (!good || good.error) problems.push('the good fixture did not render: ' + (good && good.error));
|
||||
else { const f = good.findings.filter((x) => !allowed(x) && x.kind !== 'truncated'); if (f.length) problems.push('the clean fixture was flagged: ' + f.map((x) => x.kind + ' ' + JSON.stringify(x.text) + ' by ' + x.by).join('; ')); if (!good.findings.some((x) => x.kind === 'truncated')) problems.push('the ellipsis line was not noted as truncated');
|
||||
if (good.findings.some((x) => /line [67]/.test(x.text) && x.kind === 'covered')) problems.push('a line scrolled under a pane edge with a 1 px bar was flagged as covered'); }
|
||||
if (problems.length) { console.error('overlap-check self-test FAILED:\n ' + problems.join('\n ')); if (bad) console.error(JSON.stringify(bad.findings, null, 1)); return false; }
|
||||
console.log(`overlap-check self-test passed: the pill over the caption, the clipped line, the label over the value and the text past the edge are flagged; the clean page passes, its ellipsis noted (${run.secs.toFixed(1)} s)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- the previews (the owner's set) ----------
|
||||
async function previews(pw, dir, miner, wallet) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const set = [];
|
||||
if (miner) { const u = (q) => `http://127.0.0.1:${miner.port}/t/mock/?${q}`; set.push(['miner-setup', u('scenario=fresh&screen=welcome')], ['miner-mine-live', u('scenario=live&page=overview')], ['miner-earnings', u('scenario=live&page=earnings')], ['miner-prove', u('scenario=live&page=prove')], ['miner-settings', u('scenario=live&page=settings')], ['miner-update-card', u('scenario=live&page=overview&update=ready&card=1')]); }
|
||||
if (wallet) { const u = (q) => `http://127.0.0.1:${wallet.port}/t/mock/?${q}`; set.push(['wallet-lock', u('scenario=unlock&bio=touch')], ['wallet-home', u('scenario=home&page=home')], ['wallet-send', u('scenario=home&page=send')], ['wallet-receive', u('scenario=home&page=receive')], ['wallet-settings', u('scenario=home&page=settings')], ['wallet-update-card', u('scenario=update&page=home')]); }
|
||||
const browser = await pw.chromium.launch({ args: ['--no-sandbox', '--disable-gpu'] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 }, deviceScaleFactor: 2, colorScheme: 'dark' });
|
||||
const page = await ctx.newPage(); const files = [];
|
||||
for (const [name, url] of set) for (const [w, h] of [[1280, 800], [900, 700]]) {
|
||||
await page.setViewportSize({ width: w, height: h });
|
||||
await page.goto(url + '&theme=dark', { waitUntil: 'load', timeout: 20000 });
|
||||
await page.waitForLoadState('networkidle', { timeout: 2500 }).catch(() => {}); await page.waitForTimeout(900);
|
||||
const f = join(dir, `${name}__${w}.png`); await page.screenshot({ path: f }); files.push(f);
|
||||
}
|
||||
await browser.close();
|
||||
console.log('previews: ' + files.length + ' files in ' + dir); return files;
|
||||
}
|
||||
|
||||
// ---------- remote: ship the inputs to a box and run there ----------
|
||||
function hostFile(box) { const b = join(os.homedir(), '.config', 'igneum', 'build-server'); return box === '1' ? b : b + '-' + box; }
|
||||
async function runRemote() {
|
||||
const box = String(opt('--box', '2'));
|
||||
const hf = hostFile(box); if (!existsSync(hf)) { console.error(`overlap-check: no browser here and no box host file at ${hf}; install Playwright (npm i playwright && npx playwright install chromium) or run infra/build-server/run-from-mac.sh --box ${box} <ip>`); return 2; }
|
||||
const host = readFileSync(hf, 'utf8').trim(); const key = join(os.homedir(), '.ssh', 'igneum_ed25519');
|
||||
const wt = basename(root); const remote = `/srv/builds/_overlap/${wt}`;
|
||||
const ssh = ['-i', key, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new'];
|
||||
const rs = (src, dst) => { const r = spawnSync('rsync', ['-a', '--delete', '-e', `ssh ${ssh.join(' ')}`, src.endsWith('/') ? src : src + '/', `${host}:${dst}/`], { stdio: 'inherit' }); if (r.status !== 0) throw new Error('rsync failed for ' + src); };
|
||||
spawnSync('ssh', ssh.concat([host, `mkdir -p ${remote}/tools/ci ${remote}/tools/ui-mock ${remote}/app ${remote}/brand`]), { stdio: 'inherit' });
|
||||
const files = ['tools/ci/overlap-check.mjs']; for (const f of files) spawnSync('scp', ['-q', '-i', key, join(root, f), `${host}:${remote}/${f}`], { stdio: 'inherit' });
|
||||
const rargs = [];
|
||||
if (has('--self-test')) rargs.push('--self-test');
|
||||
const siteDir = opt('--site', ''); if (siteDir) { const d = resolve(siteDir === true ? join(root, 'site') : siteDir); rs(d, `${remote}/site`); rargs.push('--site', 'site'); }
|
||||
const appsTree = opt('--apps', ''); const prev = opt('--previews', '');
|
||||
if (appsTree || prev) {
|
||||
const tree = resolve(appsTree && appsTree !== true ? appsTree : root);
|
||||
rs(existsSync(join(tree, 'tools', 'ui-mock', 'server.mjs')) ? join(tree, 'tools', 'ui-mock') : join(root, 'tools', 'ui-mock'), `${remote}/tools/ui-mock`);
|
||||
for (const d of ['app/igneum-app/ui', 'app/igneum-wallet/ui']) if (existsSync(join(tree, d))) { spawnSync('ssh', ssh.concat([host, `mkdir -p ${remote}/${d}`]), { stdio: 'inherit' }); rs(join(tree, d), `${remote}/${d}`); }
|
||||
const coin = join(tree, 'brand', 'igneum-coin-1024.png'); if (existsSync(coin)) spawnSync('scp', ['-q', '-i', key, coin, `${host}:${remote}/brand/`], { stdio: 'inherit' });
|
||||
if (appsTree) rargs.push('--apps', '.'); if (prev) rargs.push('--previews', 'previews');
|
||||
}
|
||||
for (const k of ['--widths', '--themes', '--jobs', '--only']) if (opt(k, '')) rargs.push(k, String(opt(k)));
|
||||
if (SHOTS) rargs.push('--shots', 'shots'); if (JSON_OUT) rargs.push('--json', 'findings.json');
|
||||
const cmd = `cd ${remote} && . /srv/builds/_bin/overlap/env.sh && nice -n 10 node tools/ci/overlap-check.mjs --local ${rargs.map((a) => `'${a}'`).join(' ')}`;
|
||||
console.log(`overlap-check: no browser on this machine; running on box ${box} (${host}:${remote})`);
|
||||
const r = spawnSync('ssh', ssh.concat([host, cmd]), { stdio: 'inherit' });
|
||||
const back = (what, to) => spawnSync('rsync', ['-a', '-e', `ssh ${ssh.join(' ')}`, `${host}:${remote}/${what}`, to], { stdio: 'inherit' });
|
||||
if (SHOTS) { mkdirSync(SHOTS, { recursive: true }); back('shots/', SHOTS); }
|
||||
if (JSON_OUT) back('findings.json', JSON_OUT);
|
||||
if (prev) { mkdirSync(prev, { recursive: true }); back('previews/', prev); console.log('previews copied to ' + resolve(prev)); }
|
||||
return r.status === null ? 1 : r.status;
|
||||
}
|
||||
|
||||
// ---------- main ----------
|
||||
(async () => {
|
||||
if (!has('--self-test') && !opt('--site', '') && !opt('--apps', '') && !opt('--previews', '')) { console.error('usage: overlap-check.mjs --self-test | --site [dir] | --apps [tree] | --previews <dir> [--widths ..] [--themes ..] [--shots dir] [--json file] [--local|--box N]'); process.exit(2); }
|
||||
const pw = await loadPlaywright();
|
||||
if (!pw) { if (has('--local')) { console.error('overlap-check: Playwright is not installed here (npm i playwright; npx playwright install chromium) and --local forbids the box'); process.exit(2); } process.exit(await runRemote()); }
|
||||
let code = 0; const all = [];
|
||||
if (has('--self-test')) { if (!(await selfTest(pw))) { process.exitCode = 1; return; } }
|
||||
const closers = [];
|
||||
try {
|
||||
const appsTree = opt('--apps', ''); const prev = opt('--previews', '');
|
||||
let miner = null, wallet = null;
|
||||
if (appsTree || prev) {
|
||||
const tree = resolve(appsTree && appsTree !== true ? appsTree : root);
|
||||
// both mocks travel with the app tree (their scenarios match that UI): tools/ui-mock/server.mjs and wallet.mjs
|
||||
const mockDir = existsSync(join(tree, 'tools', 'ui-mock', 'server.mjs')) ? join(tree, 'tools', 'ui-mock') : join(root, 'tools', 'ui-mock');
|
||||
if (existsSync(join(tree, 'app', 'igneum-app', 'ui', 'index.html'))) { miner = await spawnMock(join(mockDir, 'server.mjs'), null); closers.push(miner.close); } else console.log('overlap-check: no app/igneum-app/ui in ' + tree + '; the miner UI is skipped');
|
||||
if (existsSync(join(tree, 'app', 'igneum-wallet', 'ui', 'index.html')) && existsSync(join(mockDir, 'wallet.mjs'))) { wallet = await spawnMock(join(mockDir, 'wallet.mjs'), null); closers.push(wallet.close); } else console.log('overlap-check: no app/igneum-wallet/ui in ' + tree + '; the wallet UI is skipped');
|
||||
if (prev) await previews(pw, resolve(prev), miner, wallet);
|
||||
if (appsTree) { const targets = (miner ? minerTargets(miner.port) : []).concat(wallet ? walletTargets(wallet.port) : []).filter((t) => !ONLY || t.name.includes(ONLY)); if (targets.length) all.push(await renderAll(pw, targets, 'apps')); }
|
||||
}
|
||||
const siteDir = opt('--site', '');
|
||||
if (siteDir) {
|
||||
const dir = resolve(siteDir === true ? join(root, 'site') : siteDir);
|
||||
const srv = await serveSite(dir); closers.push(srv.close);
|
||||
const targets = siteTargets(dir, srv.port).filter((t) => !ONLY || t.name.includes(ONLY));
|
||||
all.push(await renderAll(pw, targets, 'site'));
|
||||
}
|
||||
} finally { for (const c of closers) try { c(); } catch (e) { /* closed */ } }
|
||||
if (JSON_OUT) writeFileSync(JSON_OUT, JSON.stringify(all, null, 1));
|
||||
for (const run of all) {
|
||||
const { fails, notes, errors } = report(run);
|
||||
const renders = run.results.length, runs = run.results.reduce((a, r) => a + (r.runs || 0), 0);
|
||||
console.log(`overlap-check (${run.label}): ${renders} renders, ${runs} text runs read, ${fails.length} overlaps, ${notes.length} notes, ${errors.length} render errors in ${run.secs.toFixed(1)} s`);
|
||||
for (const n of notes) console.log(' note ' + n);
|
||||
for (const e of errors) { console.log(' ERROR ' + e); code = 1; }
|
||||
for (const f of fails) { console.log(' FAIL ' + f); code = 1; }
|
||||
}
|
||||
process.exitCode = code; // not process.exit: stdout to a pipe (ssh, the gate's log) is asynchronous and exit would drop the tail
|
||||
})().catch((e) => { console.error('overlap-check: ' + (e.stack || e)); process.exitCode = 1; });
|
||||
|
|
@ -47,6 +47,14 @@ site_build() {
|
|||
(cd "$SITE_TMP/site" && SITE_DOWNLOADS_OFFLINE=1 node build.mjs)
|
||||
}
|
||||
|
||||
overlap_sweep() {
|
||||
# tools/ci/overlap-check.mjs: the known-failed fixture first, then the built site (the gate's temporary copy locally, the tree in
|
||||
# CI). A browser is needed: CI installs Playwright in the workflow; a machine without one ships the pages to a build box
|
||||
# (nothing heavy on the Mac). IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs through their mocks (slower, the box).
|
||||
local dir="site"; [ "$MODE" = ci ] || dir="$SITE_TMP/site"
|
||||
if [ "${IGNEUM_OVERLAP_APPS:-0}" = 1 ]; then node tools/ci/overlap-check.mjs --self-test --site "$dir" --apps .; else node tools/ci/overlap-check.mjs --self-test --site "$dir"; fi
|
||||
}
|
||||
|
||||
structural_checks() {
|
||||
run "no conflict markers in tracked files" bash tools/ci/no-conflict-markers.sh
|
||||
run "every tracked path is valid on Windows (colon, trailing dot, reserved names, length)" bash tools/ci/windows-paths-check.sh
|
||||
|
|
@ -88,7 +96,7 @@ tree_checks() {
|
|||
run "long-running tools keep their body in one parsed block (the edited-while-running class)" bash -c 'bash tools/ci/whole-body-check.sh --self-test && bash tools/ci/whole-body-check.sh'
|
||||
run "build-remote without a priority flag bounds suites and benches (nice 10, 32 cores); a gate runs unbounded" bash tools/ci/build-kind-default-check.sh
|
||||
run "the class router is a preference with spill-over (a held or overloaded box hands the job to the other one)" bash tools/ci/route-spill-check.sh
|
||||
run "per-core leases, the quiet class and the reaper pass on the box (lease.sh and remote-run.sh self-tests over ssh)" bash tools/ci/box-locks-check.sh
|
||||
run "per-core leases, the quiet class and the reaper pass on the box (lease.sh and remote-run.sh self-tests over ssh)" bash tools/ci/box-locks-check.sh $( [ "$MODE" = ci ] && echo --ci )
|
||||
run "the simulators job runs on master and release-* pushes and pull requests into them only" bash tools/ci/sims-branch-check.sh
|
||||
run "no shell assignment hides behind a trailing comment (the swallowed-defaults class)" bash -c 'bash tools/ci/defaults-line-check.sh --self-test && bash tools/ci/defaults-line-check.sh'
|
||||
run "no script kills or finds a process by a plain name or a file name (pgrep/pkill -f literals, ps | grep)" bash -c 'bash tools/ci/kill-by-name-check.sh --self-test && bash tools/ci/kill-by-name-check.sh'
|
||||
|
|
@ -102,6 +110,7 @@ tree_checks() {
|
|||
run "chain scene: a push paints with the document hidden and no animation frame (the blank /live of 7 Oct 2026; known-failed first)" node tools/scene/paint-test.cjs
|
||||
run "chain scene: the live feed contract (the recorded reply validates; a rewritten miner, a float now, a stray key refused)" node --test tools/scene/feed-contract.test.mjs
|
||||
run "chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright)" bash tools/scene/parity-remote.sh
|
||||
run "no text overlaps: every served page at 390 to 1600 px, light and dark, the hero at each step (self-test first; IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs)" overlap_sweep
|
||||
run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/api/public-stats.test.mjs
|
||||
run "ship tool self-test" node tools/ship-app.mjs --self-test
|
||||
run "relay unit tests" node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs
|
||||
|
|
|
|||