Compare commits
31 commits
master
...
testnet-ge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3722ddba7f | ||
|
|
4fb52e5e45 | ||
|
|
74e7320aa7 | ||
|
|
460490b07e | ||
|
|
f6c5d4dc53 | ||
|
|
77df3f8837 | ||
|
|
563f4fb8d9 | ||
|
|
37adea313f | ||
|
|
c8e8e11864 | ||
|
|
4b3a640cc2 | ||
|
|
c2b276d314 | ||
|
|
7d35133b26 | ||
|
|
07988628a9 | ||
|
|
e25517b6ef | ||
|
|
2228e30fb4 | ||
|
|
b0d2d3e478 | ||
|
|
8c25dad649 | ||
|
|
343109f42a | ||
|
|
08d97a849c | ||
|
|
7ef62dd712 | ||
|
|
161cc06d6a | ||
|
|
3841171a21 | ||
|
|
f42945e981 | ||
|
|
514ee824ee | ||
|
|
013c243cc6 | ||
|
|
43aad83f81 | ||
|
|
0915a7a37e | ||
|
|
5f3db40fe0 | ||
|
|
37de380bb8 | ||
|
|
7087fbb850 | ||
|
|
cbbaace4bd |
9 changed files with 1311 additions and 20 deletions
|
|
@ -2652,3 +2652,78 @@ in the same shape and reports a box behind its wanted binary.
|
||||||
| Rig | the same, and a rig that leaves is itself a weight removal: at 459 MH/s on tonight's devnet it is about 20 percent of the weight, over the hour's budget by itself |
|
| Rig | the same, and a rig that leaves is itself a weight removal: at 459 MH/s on tonight's devnet it is about 20 percent of the weight, over the hour's budget by itself |
|
||||||
| Pool | a pool node is one voter carrying its members' whole weight; a pool restart is the largest single removal on the network and must be sliced like the fleet's |
|
| Pool | a pool node is one voter carrying its members' whole weight; a pool restart is the largest single removal on the network and must be sliced like the fleet's |
|
||||||
| The network | finality by miner weight is only as steady as the miners' uptime; until public hash dwarfs the fleet, the fleet's supervisor is a consensus component |
|
| The network | finality by miner weight is only as steady as the miners' uptime; until public hash dwarfs the fleet, the fleet's supervisor is a consensus component |
|
||||||
|
|
||||||
|
## 7 October 2026, 10:08 UK: igneum-testnet-1 re-cut, the two-node 18-decimal gate (GPU form) PASS
|
||||||
|
|
||||||
|
The re-cut testnet object (genesis `01294fd3...`, digest `80af8aa1...`, every switch on from genesis; `docs/plans/testnet-go.md`
|
||||||
|
"The genesis, re-cut") on igneumd 8a6f1f56... and igneum-miner 726cf290... (fork `testnet-genesis-2-node` f1717419), the
|
||||||
|
gate script `tools/fleet/base-unit-gate.sh` at 72b02b48, run by the fleet lane on a one-shot RunPod RTX 3090.
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| both nodes agree | 577 blocks, one sink, exec tips 0x1a5 and 0x1a5 |
|
||||||
|
| blocks mined in 600 s | 577 (421 chain blocks) against the minimum 60 |
|
||||||
|
| the UTXO side, last 30 blocks | identical on both nodes; 30 payload subsidies at the 18-decimal schedule, 0 wrong; 80/20 exact on 18 of 18 single-payee coinbases |
|
||||||
|
| the execution layer | balance equal on both nodes; 577 rewards against the schedule at each blue block's own DAA, 0 wrong; the sum of the rewards equals eth_getBalance: 4,617,536,546,296,296,296,298 wei = 4,617.53654629 IGN |
|
||||||
|
| the start-up lines | `Base unit: 10^18`, digest `80af8aa1...`, ladder active at rung 0, fees v1 from DAA 0, proof verification from DAA 0 under shard `0x2b1a81cb...` and aggregator `0x474678f3...` |
|
||||||
|
|
||||||
|
Consequences: the layout, the switches and the bridge hold at 18 decimals on real GPU blocks; no tier changes (docs/design/base-unit.md
|
||||||
|
section 5 stands). The first run of the form read 165 of 561 rewards one ramp step low because the gate priced every reward at the
|
||||||
|
chain block's DAA while the object credits each merged block its own (subsidy_per_block_activation_daa 0): the script, not the node.
|
||||||
|
|
||||||
|
## 7 October 2026, 11:12 UK: igneum-testnet-1 re-cut, the object at fast time (devnet-suffix, 5 nodes) PASS
|
||||||
|
|
||||||
|
`infra/fast-time/testnet-object.mjs` run 10 on igneum-build-1 (the archive binaries, fork `testnet-genesis-2-node` 5c25c1fb; network
|
||||||
|
igneum-devnet-973 on the 60x profile with every switch of `infra/seed-nodes/testnet-object.json` from genesis, the fast-time finality
|
||||||
|
profile, leave delay 60; four voting CPU miners and one `--no-vote` key; 600 s, the leave at 240 s).
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| start-up lines, one digest | every node: ladder active at rung 0, fees v1 from DAA 0, verification from DAA 0 under the pinned ids, rule v3 and C1 from DAA 0, the leave rule from DAA 0 |
|
||||||
|
| class v4 at rung 0 | ten epochs, 27 shadow passes, no ladder bits on the chain, object byte 0 |
|
||||||
|
| the chain | 551 blocks, 525 chain blocks, 0 rejected, one sink on five nodes |
|
||||||
|
| finality | first lock at DAA 142 (172 s), lock 18 at the end, active |
|
||||||
|
| the signing bonus (UTXO side) | the silent key's 87 blocks at 72 percent of each block's own subsidy; the voters' 320 at 80 percent |
|
||||||
|
| the bridge identity | UTXO payments x 10^10 = execution credits for all five miners over the chain (the tip's own credit set aside) |
|
||||||
|
| the leave item | accepted at DAA 212, voters 5 to 4 at DAA 351 (within the delay and a window), longest pause after it 1 s |
|
||||||
|
|
||||||
|
Consequences: a miner that stops signing while in the table earns 72 percent on both ledgers, never 80 on one and 72 on the other (the
|
||||||
|
N7 fix, c7ea1e21); a side block merged one DAA late is credited its own subsidy on both ledgers (the N8 fix, d840537b, from genesis);
|
||||||
|
a miner that leaves cleanly is out of every denominator an hour after its leave is carried on the testnet (60 DAA here) and holds
|
||||||
|
nothing; nothing changes for hash rate, power or any tier's hardware.
|
||||||
|
|
||||||
|
## 7 October 2026, 14:36 UK: igneum-testnet-1 re-cut, the late-join gate (proof archive, ledger N9 second half) PASS
|
||||||
|
|
||||||
|
`infra/fast-time/tn-late-join.mjs` on a one-shot RunPod 3070 pod (os-latejoin-ylp1), the fast-time join shape of ca3-v4-node e5f993d4 with
|
||||||
|
consensus proof verification and proving v0 from genesis in the override, a CPU prover beside node A (`infra/fast-time/tn-prover-loop.mjs`).
|
||||||
|
|
||||||
|
| Side | Binary | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| node A | archive binary 57ad7dc2... (fork 5c25c1fb, aea0ca5c in) | 6,297 DAA, 37 proofs carried and verified, 34 in the archive, pruning point at DAA 1,679 |
|
||||||
|
| B, known-failed first | 57ad7dc2... (before 70e4601e) | stalled at DAA 1,828 six times, "proofs this peer did not deliver in 20 s", while A held the file |
|
||||||
|
| B, the fix | fbed53cd... (fork 26e648ff, 70e4601e in) | headers-proof IBD completed, A's sink reached in 35 s, 4,618 headers, 0 errors |
|
||||||
|
|
||||||
|
Consequences: with `proving_consensus_verify_daa` 0 from genesis a node joining igneum-testnet-1 after the pool's window syncs from the
|
||||||
|
pruning point and fetches every proof above it from any peer's archive (one 1.27 MB file per carried proof, kept for the pruning window:
|
||||||
|
108,000 DAA on the testnet, so about 30 hours of proofs, at most a few GB on a seed); before 70e4601e such a joiner never finished its
|
||||||
|
IBD. The fix is on `testnet-genesis-2-node` (26e648ff) and the node lane's `proof-hold-fix`; the 0.3.20 line takes it from there.
|
||||||
|
|
||||||
|
## 7 October 2026, 15:19 UK: igneum-testnet-1 re-cut FINAL, the two-node 18-decimal gate (GPU form) PASS on the final object
|
||||||
|
|
||||||
|
The final object (genesis `01294fd3...`, digest `4fbb2152...`, every switch on from genesis, the genesis forward-compatibility fields in;
|
||||||
|
`docs/plans/testnet-go.md` "The genesis, re-cut") on igneumd 58f96049... and igneum-miner 5625caee... (fork `testnet-genesis-2-node`
|
||||||
|
e6dd3afd), `tools/fleet/base-unit-gate.sh` 72b02b48, run by the fleet lane on a one-shot RunPod RTX A5000.
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| both nodes agree | one sink, exec tips 0x142 and 0x142 |
|
||||||
|
| blocks mined in 600 s | 322 chain blocks against the minimum 60 |
|
||||||
|
| the UTXO side, last 30 blocks | identical on both nodes; 30 payload subsidies at the 18-decimal schedule, 0 wrong |
|
||||||
|
| the execution layer | 491 rewards against the schedule at each blue block's own DAA, 0 wrong; the sum of the rewards equals eth_getBalance on both nodes: 3,929,111,342,592,592,592,602 wei = 3,929.11134259 IGN |
|
||||||
|
| the start-up lines | `Base unit: 10^18`, digest `4fbb2152...`, `igneumd/2.1.0-e6dd3afd`, genesis `01294fd3...` executed, chain id 4462 |
|
||||||
|
| the known-failed form | the 8-decimal schedule against the same chain: payloads wrong 30 of 30, rewards wrong 305 of 305, FAIL |
|
||||||
|
|
||||||
|
Consequences: the cut's four gates are green on one object (this gate, the fast-time line of 11:12 UK, the late-join gate of 14:36 UK, the
|
||||||
|
suites); what the seeds' cut-over needs is the project lead's go alone (the build-server lane's `wave1-0320.sh seeds` line is armed and dry-run clean on
|
||||||
|
these values). No tier changes: the base-unit costs of docs/design/base-unit.md section 5 stand, a Windows node needs igneum-prove-host
|
||||||
|
beside it, a miner that stops cleanly sends its leave, a silent key earns 72 percent on both ledgers.
|
||||||
|
|
|
||||||
|
|
@ -2253,7 +2253,7 @@ Status: Fixed, stated (6 October 2026, night, the owner's decision): every menti
|
||||||
|
|
||||||
Answer: The date was the plan of 3 October 2026 and the chain overtook it: the testnet genesis was fixed on 5 October, the three seeds and rpc.testnet.igneum.network are up, and the remaining work is the go checklist. Rows that quoted the month (X3, O-X.2's blocker note, overclaim item 75's replacement text) read the new sentence by reference to this row.
|
Answer: The date was the plan of 3 October 2026 and the chain overtook it: the testnet genesis was fixed on 5 October, the three seeds and rpc.testnet.igneum.network are up, and the remaining work is the go checklist. Rows that quoted the month (X3, O-X.2's blocker note, overclaim item 75's replacement text) read the new sentence by reference to this row.
|
||||||
|
|
||||||
Evidence: `docs/plans/testnet-go.md`; `docs/igneum-testnet` notes (genesis 87617621..., seeds seed1 to seed3.testnet.igneum.network, public RPC). Checked by `tools/ci/ledger-text-check.mjs` (the X3 and X31 rows).
|
Evidence: `docs/plans/testnet-go.md`; `docs/igneum-testnet` notes (genesis 01294fd3... since the re-cut of 7 October 2026, 87617621... before it; seeds seed1 to seed3.testnet.igneum.network, public RPC). Checked by `tools/ci/ledger-text-check.mjs` (the X3 and X31 rows).
|
||||||
|
|
||||||
### X32. The roadmap carried calendar months beside a testnet that is weeks away
|
### X32. The roadmap carried calendar months beside a testnet that is weeks away
|
||||||
"After X31 the roadmap read phase 4 'Apr to Jul 2027' and phase 6 'Nov 2027' with phase 5 'weeks away' between them, and phases 1 to 3 carried 'Oct to Nov 2026', 'Nov 2026 to Jan 2027' and '20 nodes by Mar 2027'. A reader spots the contradiction at once."
|
"After X31 the roadmap read phase 4 'Apr to Jul 2027' and phase 6 'Nov 2027' with phase 5 'weeks away' between them, and phases 1 to 3 carried 'Oct to Nov 2026', 'Nov 2026 to Jan 2027' and '20 nodes by Mar 2027'. A reader spots the contradiction at once."
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,13 @@
|
||||||
Prepared 5 October 2026 by the infrastructure and consensus engineer for the 19:00 BST (18:00 UTC) opening. State of
|
Prepared 5 October 2026 by the infrastructure and consensus engineer for the 19:00 BST (18:00 UTC) opening. State of
|
||||||
every line as of 16:05 UTC. Nothing mines until the owner says go; the seeds hold the chain at height 0.
|
every line as of 16:05 UTC. Nothing mines until the owner says go; the seeds hold the chain at height 0.
|
||||||
|
|
||||||
|
RE-CUT 7 October 2026 (the testnet genesis lane, on the project lead's approvals of 09:3x UK, `docs/plans/ledger-decisions.md`
|
||||||
|
"Decisions (7 October 2026, 09:3x UK)" row 5, and his words of the same morning on the two exec-side findings): the
|
||||||
|
genesis is re-cut on the 18-decimal layout with `EmissionSchedule::TESTNET_1` and every switch on from genesis; section
|
||||||
|
"The genesis, re-cut" below is the object, "The seeds' cut-over" the runbook. The three seeds still hold the 5 October
|
||||||
|
chain (genesis `87617621...`, digest `b7d8c915...`) at height 0 and move only on the project lead's go. The hash and digest are
|
||||||
|
FINAL as of 18:4x UK: mission item 8 (the genesis forward-compatibility fields, lane `genesis-forward`, daa61847 to f95178a1 and ff06c05c) is in the object.
|
||||||
|
|
||||||
## What runs now
|
## What runs now
|
||||||
|
|
||||||
| Piece | Where | State at 16:05 UTC |
|
| Piece | Where | State at 16:05 UTC |
|
||||||
|
|
@ -28,23 +35,66 @@ igneumd/2.1.0
|
||||||
`health.sh` shows `synced=False` on all three: that is the no-blocks state (a node is synced once it has blocks past
|
`health.sh` shows `synced=False` on all three: that is the no-blocks state (a node is synced once it has blocks past
|
||||||
genesis), not a fault. The check: `cd infra/seed-nodes && NET=testnet ./health.sh`.
|
genesis), not a fault. The check: `cd infra/seed-nodes && NET=testnet ./health.sh`.
|
||||||
|
|
||||||
## The genesis, final
|
## The go object (ruling of 7 October 2026, 19:2x UK): the 0.3.23 testnet object, pending the node lane's cut
|
||||||
|
|
||||||
|
By the coordinator's ruling of the evening of 7 October 2026, igneum-testnet-1 goes on the 0.3.23 testnet object (the Devnet 3 shape:
|
||||||
|
byte 7 from genesis, every Devnet 3 activation at 0, the era VDF at 0, class v5 at 0 if its Devnet 3 crossing reads clean, the heights,
|
||||||
|
the bonus's switch field) AT 18 DECIMALS (the project lead's word, 21:35 BST: the decimals lane's code merged, the schedule at 18, the 16-byte
|
||||||
|
genesis payload on the final message, the scale factor tested known-failed first), cut and re-armed on the seeds by the build-server lane, which owns them; the interim re-arm is the
|
||||||
|
release-0.3.22-node object (34a2dbaa, digest `87d103b6...`, genesis `52a3e6a9...`). Two orders ride with it: the testnet genesis carries
|
||||||
|
the FINAL message (the 5 October payload that `87617621...` carried, never the 4 October "proposed, not final" text that `52a3e6a9...`
|
||||||
|
hashes: a public node never starts on a proposal; the digest moves and the re-arm absorbs it), and the base unit is 18 by the project lead's word of 21:35 BST (the
|
||||||
|
8-decimal scratch form is no longer needed); nothing on a seed before the node lane names the cut and the build-server lane's dry run reads clean. This section's object table is rewritten against that object once the node lane names the cut; the gate rows 9g, 9h and
|
||||||
|
9j re-run on it; row 9c reads "re-arms on each cut". Everything below this paragraph in this section is the 7 October RE-CUT of this
|
||||||
|
lane, void as a go object by the ruling and kept as the record of what was built and gated.
|
||||||
|
|
||||||
|
### The 7 October re-cut (void by the ruling; the record)
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Network | `igneum-testnet-1`, chain id 4462, address prefix `igneumtest`, ports 26810 (gRPC), 26811 (p2p), 28810 (wRPC JSON), 26890 (EVM JSON-RPC) |
|
| Network | `igneum-testnet-1`, chain id 4462, address prefix `igneumtest`, ports 26810 (gRPC), 26811 (p2p), 28810 (wRPC JSON), 26890 (EVM JSON-RPC) |
|
||||||
| Coinbase message | `igneum-testnet-1 \| 2026-10-05 \| coins here have no value \| resets are announced` ("proposed, not final" dropped before the first public node) |
|
| Coinbase message | `igneum-testnet-1 \| 2026-10-05 \| coins here have no value \| resets are announced` (unchanged from 5 October) |
|
||||||
| Timestamp | 1,791,158,400,000 ms = 2026-10-05T00:00:00Z |
|
| Coinbase subsidy field | 16 little-endian bytes, one IGN = 10^18 base units (the 18-decimal layout, O-2.6) |
|
||||||
| Bits | `0x1d100000` |
|
| Timestamp | 1,791,158,400,000 ms = 2026-10-05T00:00:00Z (unchanged) |
|
||||||
| Hash | `87617621714af1bf33bd17f291f90a7e0bff760a669bba53083ea8c0f7cbd840` |
|
| Bits | `0x1d100000` (unchanged) |
|
||||||
| Merkle root | `44acfc40b1c6647011510f3c39ddb7606f979df92ad26a2914de56e44610efad` |
|
| Hash | `01294fd322704dc28fbef0e7a5ef86d6ee260ac5efaf88891cdba661b5fd58ac` |
|
||||||
| Consensus digest | `b7d8c915f20f5af261e69e7f4aa9c3d03a9c4a462174776502b63fa1e9ec8447` |
|
| Merkle root | `bcd0e8fb1099aeb3cc1370b50e6dec9f615e204374b2097f25b2438e7d22f49f` |
|
||||||
|
| Consensus digest | `63faee44f50eccd3d68c970efbc39edee2977a4f0fbf4baa833aa5ef5de3577a` (18:4x UK, with the cache rung behind its own switch, the genesis-forward lane's ff06c05c after the Devnet 3 digest finding; `4fbb2152...` at 15:0x UK with the genesis forward-compatibility fields under the ladder's arm, `80af8aa1...` before them with `subsidy_per_block_activation_daa` 0, `9390d235...` before that field) |
|
||||||
|
| Base unit | 18 decimals (`base_unit_decimals` 18): one IGN is 10^18 base units, the EVM's wei; the bridge is the identity |
|
||||||
|
| Emission | `EmissionSchedule::TESTNET_1` at the unit: 100 IGN a block at 1 bps, a monthly glide with a two-year half-life, a 90-day ramp from 10 percent, a 1 percent of supply a year tail from the month the glide first pays under it; no hard cap |
|
||||||
|
| Switches on from genesis | `difficulty_v2_activation_daa` 0, `difficulty_v3_activation_daa` 0, `proving_v0_activation_daa` 0, `finality_v3_activation_daa` 0, `finality_daa_rule_activation_daa` 0, `finality_leave_activation_daa` 0 (`finality.leave_delay` 3,600), `signing_bonus_activation_daa` 0 with `signing_bonus_bps` 1,000 (no burn; vote-or-burn is out of the tree, 420f9305), `program_class_v3_activation_daa` 0, `program_class_v4_activation_daa` 0 with signal window 0 (v4 unconditional), `latency_ladder_activation_daa` 0 at rung 0 with `latency_ladder_window_daa` 86,400 and the six rungs 27 (admissible), 35 (admissible), 53 (admissible), 88 (inadmissible: quiet-core re-measure 08:46 UK, 10.85 ms cold with the sibling loaded, over the 10 ms gate), 173 and 267 (inadmissible), `fees_v1_activation_daa` 0 (`FeeParams::CALIBRATED_V1`), `proving_consensus_verify_daa` 0 under shard program id `0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a` and aggregator id `0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896` (`proving/igneum-prove/elf/manifest.json`, pinned 2026-10-05T16:20:38Z), `subsidy_per_block_activation_daa` 0 (each merged block credited the subsidy of its own DAA on the EVM side, as the UTXO coinbase pays it), and mission item 8 (the project lead's order, 10:1x UK; `docs/design/genesis-forward.md`): `sig_scheme` 0 (BLS12-381) with `sig_scheme_activation_daa` 0 (every vote item and key reveal carries the scheme byte on the wire; any other scheme is refused by every node until a program class the 95 percent signal moves to names it), `finality_succession_activation_daa` 0 (the W5 item: a vote key hands its window weight and its forfeit term to a successor once), `latency_ladder_cache_rung` {mib 512, admissible false} beside the six N rungs with `latency_ladder_cache_rung_activation_daa` 0 (inadmissible until measured; its own digest arm after the ladder's, ff06c05c) |
|
||||||
|
| Left at never, by design | `fork_gate_activation_daa` (window 600), `proving_v1_activation_daa` and `proving_v1_fresh_rule_daa`, `exec_restart_*` |
|
||||||
|
| Finality object | `FinalityParams::MAINNET`: interval 30, depth 60, window 2,592,000 DAA, dust 100, presence 240, 8 aggregators, ban 2,592,000, min DAA 2,592,000, fold 6, leave delay 3,600 |
|
||||||
| Fees | `CALIBRATED_V1` from DAA 0 |
|
| Fees | `CALIBRATED_V1` from DAA 0 |
|
||||||
| DNS seeders in `TESTNET_PARAMS` | `seed1.testnet.igneum.network`, `seed2.testnet.igneum.network`, `seed3.testnet.igneum.network` |
|
| DNS seeders in `TESTNET_PARAMS` | `seed1.testnet.igneum.network`, `seed2.testnet.igneum.network`, `seed3.testnet.igneum.network` |
|
||||||
| `--netsuffix` | defaults to 1 under `--testnet` |
|
| `--netsuffix` | defaults to 1 under `--testnet` |
|
||||||
|
| Override file | refused on the testnet (as on mainnet): the object above is compiled into `TESTNET_PARAMS`; `infra/seed-nodes/testnet-object.json` is the same object as override-file JSON (`print_testnet_object`), for reading and diffing against the daemon's start-up lines, never loaded |
|
||||||
|
| Where it lives | node fork branch `testnet-genesis-2-node` on the box mirror (`release-0.3.18-node` e69e8a39, the decimals branch df2fbd03 merged, the vote-or-burn removal 420f9305, the exec-side fixes c7ea1e21 and d840537b, the carried-proof gating dc141409, the proof archive aea0ca5c, the proof-hold fix 70e4601e, the genesis-forward commits daa61847 to f95178a1 and ff06c05c, the re-cut); repository branch `testnet-genesis-2` |
|
||||||
|
|
||||||
The proposal's hash `52a3e6a9...` is void: it hashed the old message. Any node built from a fork commit before 1c19441d
|
Void: the 5 October genesis `87617621...` (8-byte subsidy layout; the chain the seeds hold until the cut-over), its digest `b7d8c915...`, the proposals `52a3e6a9...` and `494fc9a3...`, and the interim digests `9390d235...`, `80af8aa1...` and `4fbb2152...`. Any node built before the re-cut never completes a handshake with a re-cut node (different genesis, different digest; seen 7 October 2026, 08:05 UK: a box node on the re-cut tree dialled the three live seeds and each side refused the other on the digest).
|
||||||
has the old genesis and never completes a handshake with the seeds (different genesis, different digest).
|
|
||||||
|
What a node prints at start on this object (igneum-build-1, 7 October 2026, 08:05 UK, the two-node gate's node A):
|
||||||
|
|
||||||
|
```
|
||||||
|
Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
|
||||||
|
Fees on igneum-testnet-1: pgas table v1, B_p 120000 pgas, S_p 30000 pgas, floors 100000000000 wei per gas and 10000000000000 wei per pgas; calibrated v1 from DAA score 0
|
||||||
|
Base unit: 10^18 base units per IGN (wei, the EVM's unit); one base unit is 1 wei on the execution layer
|
||||||
|
Consensus params digest: <the digest above> (exchanged in the p2p handshake; a peer with another digest is refused)
|
||||||
|
Finality v2 (igneum-testnet-1): interval 30 depth 60 window 2592000 DAA dust 100 presence 240 aggregators 8 ban 2592000 fold 6; rule v3 (frozen table, certificate fold) from checkpoint DAA 0; ... C1 in DAA seconds
|
||||||
|
Proving: consensus proof verification from DAA score 0 (shard program id 0x2b1a81cb..., aggregator id 0x474678f3...); a carried record whose proof fails invalidates its block
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consequences of the object, per tier (the standing rule of 5 October 2026)
|
||||||
|
|
||||||
|
| Number | What it means | Who it touches |
|
||||||
|
|---|---|---|
|
||||||
|
| 18 decimals, 16-byte amounts | a node holds about 8 percent more UTXO bytes on disk and 11 percent more in memory than at 8 (docs/design/base-unit.md section 5: +80 MB disk, +170 MB memory at 10,000,000 UTXOs); no tier changes class on 8 GB to 32 GB cards, Windows, Linux or macOS; wallets and exchanges see Ethereum's unit | every node; nothing for hash rate, power or a miner's deadline |
|
||||||
|
| 100 IGN a block, a 90-day ramp from 10 percent | day-0 block pays 10 IGN (8 to the producer, 2 to the proving pool); the first full-rate block is above u64::MAX in base units, which is why every amount is u128 | every miner's expectation of the first month |
|
||||||
|
| the signing bonus at 1,000 bps from genesis | a vote key that stops signing while in the weight table earns 72 percent of the subsidy instead of 80; a home miner on one card who runs the app as shipped signs every checkpoint and is untouched; a pool user's key is the pool's | silent keys only; both ledgers agree after c7ea1e21 |
|
||||||
|
| `proving_consensus_verify_daa` 0 | every node verifies carried proof records from block one; a Windows node verifies through the installed `igneum-prove-host` and refuses to start without it (the in-process SP1 verifier is Unix-only); the seeds are Linux and verify in process; PC 1 and every Windows miner need the host beside the node (the app ships it) | Windows nodes; the go list below |
|
||||||
|
| the latency ladder from genesis at rung 0 | the shadow block costs 27 passes as class v4 ships; a step to rung 1 needs 90 percent in each of seven day-long windows, so nothing moves in the first week; rungs 4 and 5 can never be entered | every miner's hash rate is the class v4 rate; verifiers under 10 ms cold at rungs 0 to 2 |
|
||||||
|
| the leave item from genesis, delay 3,600 | a miner that stops cleanly sends a leave and is out of every denominator an hour later; one that stops without a word holds its weight in the table for the 30-day window | pool operators and rigs that restart; the app sends the leave on a clean stop |
|
||||||
|
| `FinalityParams::MAINNET` | the first lock needs 30 days of weight, so the testnet shows no lock in its first month and the exec-side identity under the bonus can only be read after it; the devnet-suffix fast-time line is the proof of that identity before go | everyone reading the finality page in the first month |
|
||||||
|
|
||||||
## Branches and commits (nothing pushed, nothing merged)
|
## Branches and commits (nothing pushed, nothing merged)
|
||||||
|
|
||||||
|
|
@ -53,6 +103,8 @@ has the old genesis and never completes a handshake with the seeds (different ge
|
||||||
| node fork (`vendor/igneum-node-testnet-infra`, from `release-0.3.6` 2b6d23ef) | `testnet-infra` | 1c19441d | final genesis message, hash and merkle root; `--netsuffix` default 1; the three DNS seeders; `igneum_testnet_identity` and `test_genesis_hashes` updated |
|
| node fork (`vendor/igneum-node-testnet-infra`, from `release-0.3.6` 2b6d23ef) | `testnet-infra` | 1c19441d | final genesis message, hash and merkle root; `--netsuffix` default 1; the three DNS seeders; `igneum_testnet_identity` and `test_genesis_hashes` updated |
|
||||||
| app repository (`igneum-wt-testnet-infra`, from master 23d11d5) | `testnet-infra` | 358e565 | `infra/seed-nodes`: `NET=testnet` profile, `seeds-testnet.tsv`, `dns.sh`, `rpc/` (filter, unit, nginx site), `node/install-rpc.sh`, `install-rpc-from-mac.sh`, ports from `seed.env`, glibc check, debian-13 images, quoted env values; `tools/build-job.mjs` forwards `--node-tests`/`--app-tests` and its watcher reads the SUMMARY wherever it sits; `docs/testnet/README.md` final genesis; this file |
|
| app repository (`igneum-wt-testnet-infra`, from master 23d11d5) | `testnet-infra` | 358e565 | `infra/seed-nodes`: `NET=testnet` profile, `seeds-testnet.tsv`, `dns.sh`, `rpc/` (filter, unit, nginx site), `node/install-rpc.sh`, `install-rpc-from-mac.sh`, ports from `seed.env`, glibc check, debian-13 images, quoted env values; `tools/build-job.mjs` forwards `--node-tests`/`--app-tests` and its watcher reads the SUMMARY wherever it sits; `docs/testnet/README.md` final genesis; this file |
|
||||||
| app repository (`igneum-wt-testnet-app`, from `testnet-infra` 9fa2bb3) | `testnet-app` | c00df85 | the app's network setting: `Packaged.network/peers/public_rpc`, `Network` enum with the testnet's ports, seeds and node directory, `Runtime::from_env_and_packaged`, the dashboard's chain label `testnet-1` with the public RPC on its tooltip, the testnet node keeps its DNS seeders; `packaged-config.sh` `IGNEUM_PACKAGE_NETWORK` (default testnet; the fleet's devnet builds pass `devnet`) |
|
| app repository (`igneum-wt-testnet-app`, from `testnet-infra` 9fa2bb3) | `testnet-app` | c00df85 | the app's network setting: `Packaged.network/peers/public_rpc`, `Network` enum with the testnet's ports, seeds and node directory, `Runtime::from_env_and_packaged`, the dashboard's chain label `testnet-1` with the public RPC on its tooltip, the testnet node keeps its DNS seeders; `packaged-config.sh` `IGNEUM_PACKAGE_NETWORK` (default testnet; the fleet's devnet builds pass `devnet`) |
|
||||||
|
| node fork (`vendor/igneum-node-testnet-genesis` in `igneum-wt-testnet-genesis`, from `release-0.3.18-node` e69e8a39; on the box mirror) | `testnet-genesis-2-node` | see the branch | the re-cut genesis and object (section "The genesis, re-cut"), the decimals branch df2fbd03 merged, 420f9305 (vote-or-burn out), c7ea1e21 and d840537b (the exec-side identity under the bonus and the per-block subsidy) carried in Amount, `igneum_testnet_identity` pins the hash and the digest, `print_testnet_object` prints the object |
|
||||||
|
| app repository (`igneum-wt-testnet-genesis`, from master ab99e5e3) | `testnet-genesis-2` | see the branch | `infra/seed-nodes/testnet-object.json`, `tools/fleet/base-unit-gate.sh` (from the decimals branch), `infra/fast-time/testnet-object.mjs` (the object at fast time: class v4, the ladder at rung 0, the first lock, the bonus split on both ledgers, a signed leave), `igneum-pow` at the 0.3.18 release line, `docs/testnet/README.md`, this file |
|
||||||
| app repository (`igneum-wt-testnet-wallet`, from `wallet-v1` a238781) | `testnet-wallet` | fe6e5e8 | `igneum-common`: `Packaged.network`, `TESTNET_PUBLIC_RPC`, `chain_id`, `effective_public_rpc`; the wallet engine reads the effective URL; the wallet's packaged config follows `IGNEUM_PACKAGE_NETWORK` |
|
| app repository (`igneum-wt-testnet-wallet`, from `wallet-v1` a238781) | `testnet-wallet` | fe6e5e8 | `igneum-common`: `Packaged.network`, `TESTNET_PUBLIC_RPC`, `chain_id`, `effective_public_rpc`; the wallet engine reads the effective URL; the wallet's packaged config follows `IGNEUM_PACKAGE_NETWORK` |
|
||||||
|
|
||||||
Tests: `kaspa-consensus-core` and `igneum-app` suites on PC 2, build job `build-20261005-155547`, both exit 0
|
Tests: `kaspa-consensus-core` and `igneum-app` suites on PC 2, build job `build-20261005-155547`, both exit 0
|
||||||
|
|
@ -65,7 +117,7 @@ Mac: 27 passed (the crate is not in the PC build inputs). The packager's self-te
|
||||||
| # | Step | Who presses | State at 16:05 UTC |
|
| # | Step | Who presses | State at 16:05 UTC |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | Seeds up at height 0, peered, the public RPC answering | nobody (done) | DONE: three seeds, 2 peers each, RPC live |
|
| 1 | Seeds up at height 0, peered, the public RPC answering | nobody (done) | DONE: three seeds, 2 peers each, RPC live |
|
||||||
| 2 | Merge `testnet-infra` and `testnet-app` to master; the fork's `testnet-infra` into `release-0.3.6` (or the release branch the 0.4.0 cut uses) | the release engineer, on the project lead's word | NOT DONE: branches ready, nothing merged |
|
| 2 | Merge `testnet-genesis-2` (the repository branch: the object record, the gate, the fast-time harness, these docs) and `testnet-app` to master; the fork's `testnet-genesis-2-node` into the release branch the 0.4.0 cut uses (0.3.19 or later; `testnet-infra` on the fork is superseded by it) | the release engineer, on the project lead's word | NOT DONE: branches ready (7 October 2026), nothing merged; waits for mission item 8 (the forward-compatibility fields) |
|
||||||
| 3 | Cut 0.4.0 from `testnet-app` (`tools/ship-app.mjs 0.4.0 --node vendor/igneum-node-testnet-infra ...`); the Mac DMG, the Windows installer through the PC build job; the packaged file must say `"network": "testnet"` (the default) | the release engineer | NOT DONE: the packager writes the testnet by default; the fleet's devnet update, if any, needs `IGNEUM_PACKAGE_NETWORK=devnet` |
|
| 3 | Cut 0.4.0 from `testnet-app` (`tools/ship-app.mjs 0.4.0 --node vendor/igneum-node-testnet-infra ...`); the Mac DMG, the Windows installer through the PC build job; the packaged file must say `"network": "testnet"` (the default) | the release engineer | NOT DONE: the packager writes the testnet by default; the fleet's devnet update, if any, needs `IGNEUM_PACKAGE_NETWORK=devnet` |
|
||||||
| 4 | The prover's fee-table mirror at `CALIBRATED_V1` (`proving/igneum-prove/core/src/config.rs`, `pgas.rs`; `docs/plans/release-0.3.6.md` section 5 step 6): on the testnet fees are v1 from genesis, so a prover with the prototype table produces shard statements the node refuses. Needed before the first testnet proof, not before the first block | the execution engineer | NOT DONE |
|
| 4 | The prover's fee-table mirror at `CALIBRATED_V1` (`proving/igneum-prove/core/src/config.rs`, `pgas.rs`; `docs/plans/release-0.3.6.md` section 5 step 6): on the testnet fees are v1 from genesis, so a prover with the prototype table produces shard statements the node refuses. Needed before the first testnet proof, not before the first block | the execution engineer | NOT DONE |
|
||||||
| 5 | History rewrite (`docs/plans/history-rewrite.md`, G14: the 40 commits with a personal name) | the project lead, then the repository goes public | NOT DONE |
|
| 5 | History rewrite (`docs/plans/history-rewrite.md`, G14: the 40 commits with a personal name) | the project lead, then the repository goes public | NOT DONE |
|
||||||
|
|
@ -73,11 +125,73 @@ Mac: 27 passed (the crate is not in the PC build inputs). The packager's self-te
|
||||||
| 7 | Downloads public: the 0.4.0 manifest in the downloads folder, `publish-manifest.sh --deploy` | the release engineer | NOT DONE |
|
| 7 | Downloads public: the 0.4.0 manifest in the downloads folder, `publish-manifest.sh --deploy` | the release engineer | NOT DONE |
|
||||||
| 8 | The site's buttons: the download section points at 0.4.0, `site/wallet.html` carries `https://rpc.testnet.igneum.network` and chain id 4462 in place of the placeholder, the terms card (`#testnet-terms`) stays; `node site/build.mjs`, push to master (Vercel deploys) | the site agent | NOT DONE: the placeholder is still in `site/wallet.html` |
|
| 8 | The site's buttons: the download section points at 0.4.0, `site/wallet.html` carries `https://rpc.testnet.igneum.network` and chain id 4462 in place of the placeholder, the terms card (`#testnet-terms`) stays; `node site/build.mjs`, push to master (Vercel deploys) | the site agent | NOT DONE: the placeholder is still in `site/wallet.html` |
|
||||||
| 9 | Announcement text | the project lead | PLACEHOLDER: "Igneum testnet-1 is open. Coins here have no value. Resets are announced seven days ahead. Download: igneum.network. RPC: rpc.testnet.igneum.network, chain id 4462." (the project lead's words replace this) |
|
| 9 | Announcement text | the project lead | PLACEHOLDER: "Igneum testnet-1 is open. Coins here have no value. Resets are announced seven days ahead. Download: igneum.network. RPC: rpc.testnet.igneum.network, chain id 4462." (the project lead's words replace this) |
|
||||||
| 10 | The first miner: one app on the testnet (PC 1 or PC 2 with the 0.4.0 build, or `igneumd --testnet` plus `igneum-miner --network testnet` by hand) produces block 1; the seeds relay it, `health.sh` shows blocks=1 on all three, `synced=True` | the project lead says go, the miner-community lead starts it | NOT DONE: nothing mines until the word |
|
| 9a | The seeds' cut-over to the re-cut genesis (the runbook below): the re-cut binary on the three seeds, each data directory wiped (the 5 October chain at height 0 has nothing to keep), the digest line read back on each, the mesh re-formed | the infrastructure engineer, on the project lead's go | NOT DONE: binaries built, nothing deployed; the seeds hold the 5 October chain |
|
||||||
|
| 9b | Proof retention: every node keeps proofs for the pruning window and the carried-proof rule applies only above the pruning point, so a node joining after the pool's horizon syncs from the pruning point (the coordinator's direction, 7 October 2026, 10:0x UK). Node side done: dc141409 (the rule, the IBD fetch and the relay retry apply from `proving_consensus_verify_daa` only) and aea0ca5c (`ProofArchive`: one file per proof under the exec db's `proofs/`, kept for the pruning window, served when the pool no longer holds the entry), both on `testnet-genesis-2-node`. The gate: `infra/fast-time/tn-late-join.mjs --join-after 700 --prover "node infra/fast-time/tn-prover-loop.mjs ..."` (a fresh node joins after the pool's 600-block window has passed, every proof it asks for from A's archive; the known-failed side is a binary before aea0ca5c, which stalls on "proofs this peer did not deliver in 20 s") | the node lane (done), then this lane | PASS at 14:36 UK, 7 October 2026 (pod os-latejoin-ylp1, RunPod 3070): node A on the archive binary 57ad7dc2... (fork 5c25c1fb) mined to DAA 6,297 at fast time (pruning 4,600, window 150) with a CPU prover beside it (igneum-prove-host, 37 records accepted and verified by A's pool in under 2 s each, 34 proofs in A's archive, one file per carrying DAA, the pool's 600-block window long past); A's miner and prover stopped; B fresh on the fixed binary fbed53cd... (fork 26e648ff, the node lane's 70e4601e: a served proof is held by hash before the native checks) joined through the headers proof and reached A's sink in 35 s, 4,618 blocks and headers, IBD completed, sink version 1026, 0 errors. Known-failed first, the same chain at 14:02 to 14:12 UK with B on the pre-fix binary 57ad7dc2...: B stalled at chain block f4f918f7 (DAA 1,828, above the pruning point at 1,679) six times on "carries 1 proof records whose proofs this peer did not deliver in 20 s" while A's archive held the file 00000000000000001828-6e3461a3...; the cause was B's fetch side (the served record refused against B's trailing exec state before the proof was held), not A's serve. Pod facts: a RunPod 3070 community pod gives about 19 vCPU and 24 GB; two CPU SP1 provers beside two nodes do not fit (every compressed proof killed at the cap), one does (7 of 36 proofs still died at the cap as the exporter's input grew with the chain); the harness's join-after target is in DAA (the unpruned block count plateaus once the pruning point moves) and a `--resume` mode attaches to a live A |
|
||||||
|
| 9e | The proving pin: `TESTNET_PARAMS` pins the shard and aggregator ids of `proving/igneum-prove/elf/manifest.json` as master holds it (shard `0x2b1a81cb...`, pinned 2026-10-05T16:20:38Z). The fin-proof lane's worktree carries a re-pin (shard `0x39db9d96...`, pinned 2026-10-07T08:03:43Z, not on master). If that re-pin merges before the go, the two ids and the digest move once more (one `print_testnet_object` run, this lane's) | whoever merges the re-pin tells this lane | OPEN |
|
||||||
|
| 9i | The suites on the final tree (fork bfcaf6a2, 15:1x UK, bounded on build-2): consensus-core 144, consensus 123 (plus the one pre-existing red of 9f), exec 31, mining 52, p2p-flows 37, pow 16, rpc-core 134, txscript 158, kaspad 2; the five gate tests (the identity test, the genesis hashes, the print, the genesis-forward digest test, the silent-split equality) 5 passed at the gate class on build-1, digest `4fbb2152...`. Earlier in the day on the interim trees: grpc-core 14, p2p 23, miner 19, utxoindex 9, index-core 9, database 22, pskt 5 | this lane | GREEN |
|
||||||
|
| 9f | A pre-existing red, not the object's: `processes::pruning_proof::igneum_m20_tests::witnesses_are_checked_in_epoch_order_under_their_own_seeds` fails under `--features igneum-pow` on the untouched `release-0.3.18-node` e69e8a39 (08:52 UK, box load under 10) and on the decimals base eec34ac3 (that lane's record); no lane's suite compiles the feature-gated test. Owner: the m20 tests' lane | the consensus engineer | OPEN, recorded |
|
||||||
|
| 9c | Mission item 8: the genesis forward-compatibility fields (the sig_scheme byte, the W5 key-succession item, the cache rung on the ladder behind its own switch), lane `genesis-forward` (daa61847 to f95178a1 and ff06c05c on the fork; its gates: consensus-core 124, consensus 114 twice on build-2, the fast-time harness's known-failed FAIL and pass PASS on build-1). The testnet object RE-ARMS ON EACH CUT (the ruling of 19:2x UK): whatever the 0.3.23 object compiles for these four is what the seeds take | lane `genesis-forward`, the build-server lane re-arms | re-arms on each cut (this lane's re-cut had the four at 0, digest `63faee44...`) |
|
||||||
|
| 9k | The genesis message: the go object's genesis carries the FINAL 5 October message, never the 4 October proposal's text (the coordinator's order, 19:2x UK, to the node lane through the build-server lane); the hash and the digest follow it | the node lane, the build-server lane re-arms | ORDERED 19:2x UK, not yet read back |
|
||||||
|
| 9l | The base unit on the testnet: 18 decimals, the project lead's word at 21:35 BST 7 October 2026 (through main). The go object is re-cut on the 0.3.23 line (7c7489ac's node pin 2720d8d2, or the 0.3.24 line if the v5 object lands first; the shipper names which) at 18: the decimals lane's code merged, the EmissionSchedule and every per-tier row at 18, the UTXO/EVM scale factor fixed and tested known-failed first on an 8-decimal fixture, the FINAL 5 October genesis message, the cache-rung field at 0, class v5 at 0 if its Devnet 3 crossing reads clean, chain id 4462. This lane's 7 October re-cut (fork testnet-genesis-2-node 59d05bf2) is that work on the 0.3.18 line and was handed to the node lane for the cherry-pick; its gates (9g, 9h, 9j) are the 18-decimal record until the re-runs on the go object | the project lead (decided), the node lane re-cuts, the build-server lane re-arms | DECIDED 21:35 BST: 18 |
|
||||||
|
| 9m | GitHub: the igneum-labs (igneum-labs) account is suspended since 18:02 BST 7 October 2026 (pushes 403, "Your account is suspended"); the project lead's ticket is open; every lane lands on the box mirror (`build`) with the box gate as the verdict until it lifts; this file's branch is on the mirror and the Mac, GitHub at 1f4793c1 | the project lead | OPEN |
|
||||||
|
| 9d | Windows: `igneum-prove-host` beside every Windows node (verification from genesis); PC 1's node refuses to start without it | the release engineer (the 0.4.0 package) | NOT DONE: to check in the 0.4.0 Windows installer |
|
||||||
|
| 9j | The two-node 18-decimal gate on the FINAL binaries (fork e6dd3afd, igneumd 58f96049..., igneum-miner 5625caee...; `tools/fleet/base-unit-gate.sh` 72b02b48, the GPU form on a RunPod RTX A5000 rented and run by the fleet lane): PASS at 15:19 UK, 7 October 2026: 322 chain blocks in 600 s, both nodes on one sink and one exec tip (0x142), the 30 inspected payloads all the 18-decimal schedule, 491 segment rewards against the schedule at each blue block's own DAA with 0 wrong, eth_getBalance 3,929.11134259 IGN (3,929,111,342,592,592,592,602 wei) equal on both nodes and to the sum of the rewards; both node heads print `Base unit: 10^18`, the digest `4fbb2152...` and `igneumd/2.1.0-e6dd3afd`, node B's exec line the genesis `01294fd3...`. The known-failed form (the 8-decimal schedule, 420 s) FAILED as it must at 15:27 UK: payloads wrong 30 of 30, rewards wrong 305 of 305. The logs: ~/igneum-fleet/tn-gate/out/ on the Mac | the fleet lane ran it, this lane reads it | PASS (the cut's closing gate) ON THE 7 OCTOBER RE-CUT; RE-RUNS ON THE GO OBJECT (the ruling of 19:2x UK) |
|
||||||
|
| 9h | The two-node 18-decimal gate on the re-cut binaries (`tools/fleet/base-unit-gate.sh` 72b02b48, the GPU form: RunPod RTX 3090, driver 580.65.06, the hive CUDA worker, rented and run by the fleet lane): PASS at 10:08 UK, 7 October 2026, on igneumd 8a6f1f56... and igneum-miner 726cf290... (fork commit f1717419): 577 blocks in 600 s, 421 chain blocks, both nodes on one sink and one exec tip (0x1a5), the 30 inspected coinbase payloads all the 18-decimal schedule (day-0 block 10 IGN = 10^19 base units), 80/20 exact on 18 of 18 single-payee coinbases, 577 segment rewards checked against the schedule at each blue block's own DAA with 0 wrong, eth_getBalance 4,617.53654629 IGN (4,617,536,546,296,296,296,298 wei) equal on both nodes and to the sum of the rewards; both node heads carry `Base unit: 10^18`, the digest `80af8aa1...`, the ladder active at rung 0, fees v1 from DAA 0 and proof verification from DAA 0 under the pinned ids. The first run of the same form (09:4x UK) read 165 of 561 rewards one ramp step low: the gate's check 5 still priced every reward at the chain block's DAA, fixed in 72b02b48 (each blue block its own DAA, the per-block rule). On the testnet object itself no voter exists inside a run (the 30-day window), so this line shows the layout, the switches and the identity without the bonus; the devnet-suffix line (9g) is the proof of the identity under the bonus | the fleet lane ran it, this lane reads it | PASS; the known-failed form (GATE_EXPECT_DECIMALS=8, the 8-decimal schedule against the same chain, 420 s) FAILED as it must at 10:15 UK: payload subsidies wrong 30 of 30, every segment reward wrong; the pod destroyed on this lane's done line ON THE 7 OCTOBER RE-CUT; RE-RUNS ON THE GO OBJECT (the ruling of 19:2x UK) |
|
||||||
|
| 9g | The fast-time line of the object (`infra/fast-time/testnet-object.mjs`, a devnet-suffix network on igneum-build-1 at the 60x profile: 5 nodes, four voting keys and one `--no-vote` key, every switch of `testnet-object.json` from genesis, the fast-time finality profile with leave delay 60, the testnet schedule at the devnet's unit): PASS at 11:12 UK, 7 October 2026, run 10 on the archive binaries (igneumd 57ad7dc2..., fork 5c25c1fb): the start-up lines and one digest on every node; class v4 at rung 0 (27 passes) through ten epochs, no ladder bits, the object byte 0 on 551 blocks; 0 rejected, one sink on five nodes; the first lock at DAA 142 (172 s); the silent key's 87 blocks paid the bonus split on the UTXO side (72 percent of the subsidy at each block's own DAA, 720,118,333 sompi at DAA 142 against 800,131,481 for a voter) and the voters' 320 blocks the plain split; the bridge identity exact for all five miners over the chain (UTXO payments x 10^10 = execution credits less the tip's own: 96,832,949,992 sompi against 968,329,499,920,000,000,000 wei for voter a, and so on); a signed leave accepted at DAA 212, its key out of the voter count (5 to 4) at DAA 351, no pause longer than one second after it, finality active at the end at lock 18. The road to it: run 1 (pre-fix binaries) showed the executor crediting 80 percent to the silent key while the coinbase paid 72 (the N7 finding), run 7 the side blues credited one ramp step high when the per-block switch was left out of the profile (the N8 shape), run 9 every check green but the harness's own pause reading (one-second reason flickers at each new lock, corrected to consecutive seconds). On the testnet object itself the weight window is 30 days, so this line is the proof of the bonus and the identity before the go | this lane | PASS; the known-failed forms are on record from the same morning: run 1 on the pre-fix binaries (the silent key credited 80 percent on the EVM side against 72 on the UTXO side, every other check green) and run 7 on the fixed binaries with the per-block switch left out of the profile (every miner's credit above its payments by the side-blue steps). A binary that carries the per-block field without the bonus fix does not exist (the pre-fix binaries refuse an override naming the field), so no third form runs ON THE 7 OCTOBER RE-CUT; RE-RUNS ON THE GO OBJECT (the ruling of 19:2x UK) |
|
||||||
|
| 10 | The first miner: one app on the testnet (PC 1 or PC 2 with the 0.4.0 build, or `igneumd --testnet` plus `igneum-miner --network testnet` by hand) produces block 1; the seeds relay it, `health.sh` shows blocks=1 on all three, `synced=True`; note the young-window join fault: a node that joins a chain younger than its finality window sees `synced=False` until blocks pass genesis, which is the no-blocks state, not a fault | the project lead says go, the miner-community lead starts it | NOT DONE: nothing mines until the word |
|
||||||
| 11 | Watch: `NET=testnet ./health.sh --watch`, the RPC's `eth_blockNumber`, the DAA after 600 blocks (the launch difficulty `0x1d100000` is sized for a few hundred MH/s) | the infrastructure engineer | ready |
|
| 11 | Watch: `NET=testnet ./health.sh --watch`, the RPC's `eth_blockNumber`, the DAA after 600 blocks (the launch difficulty `0x1d100000` is sized for a few hundred MH/s) | the infrastructure engineer | ready |
|
||||||
|
|
||||||
Legal is out of scope here (the project lead: "all but legal").
|
Legal is out of scope here (the project lead: "all but legal").
|
||||||
|
|
||||||
|
## The seeds' cut-over to the re-cut genesis (prepared 7 October 2026; runs only on the project lead's go)
|
||||||
|
|
||||||
|
Nothing below has been run against a seed. The seeds take no override file, so the cut-over is one binary and one
|
||||||
|
wipe per seed. Order: seed 1 last (it serves the public RPC), the other two first, an hour between is not needed (the
|
||||||
|
chain is at height 0 and no miner exists).
|
||||||
|
|
||||||
|
| # | Step | Command (from `infra/seed-nodes`, `NET=testnet`) | Read back |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | Build the seed binary from the fork branch `testnet-genesis-2-node` on the box for Debian 13 (glibc 2.41 on the seeds; a native 2.39 build also runs there, the 5 October seeds took a PC build at 2.39) | `cd vendor/igneum-node-testnet-genesis && ../../tools/build-remote.sh --ship seed` (artefacts in `target-remote/`), copy `igneumd` and `igneum-miner` into `infra/cross/out/` | `igneumd --version` names the commit; `sha256sum` recorded in this file at the go |
|
||||||
|
| 2 | Stop the unit and wipe the data directory on seed 2 and seed 3 (the 5 October chain is genesis alone) | `ssh root@<seed> 'systemctl stop igneumd && rm -rf /var/lib/igneum/igneum-testnet-1'` (the directory name is the network id under APPDIR; check with `ls /var/lib/igneum` first; wipe only that directory) | the directory is gone |
|
||||||
|
| 3 | Install the binary and start | `NET=testnet BUILD_WHERE=cross ./provision-seed.sh seed2.testnet`, then `seed3.testnet` (it uploads `infra/cross/out/igneumd`, refuses a glibc the seed cannot run, writes the unit and starts it) | the unit's log: `Base unit: 10^18`, `Consensus params digest: <the digest in "The genesis, re-cut">`, `genesis 01294fd3...` in the `[igneum-exec]` line, `Proving: consensus proof verification from DAA score 0`, `Latency ladder active` |
|
||||||
|
| 4 | Seed 1: the same two steps, then the public RPC check | `NET=testnet ./health.sh`; `curl https://rpc.testnet.igneum.network -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'` reads `0x0`; `eth_chainId` reads `0x116e` | three seeds, 2 peers each, height 0, every digest line equal |
|
||||||
|
| 5 | The cross-check that the old chain is gone | a 5 October binary (`igneumd 1c19441d`) pointed at a seed is refused at the handshake on the digest | one refusal line in the seed's log |
|
||||||
|
| 6 | Record | the digest, the genesis hash and the three sha256 lines in this file and in `docs/testnet/README.md`; the announcement text carries the genesis hash | |
|
||||||
|
|
||||||
|
Steps 2 to 5 as one staged command (the build-server lane, 15:1x UK; `infra/build-server/wave1-0320.sh` on branch `build-server`,
|
||||||
|
mode `seeds`, on master at 84155005; dry run by default, nothing touched; `--go` only on the project lead's word):
|
||||||
|
|
||||||
|
```
|
||||||
|
# from the build-server worktree; the seed-class binary from this lane's fork: cd vendor/igneum-node-testnet-genesis && tools/build-remote.sh --ship seed
|
||||||
|
infra/build-server/wave1-0320.sh seeds --igneumd <path to the seed-class igneumd of testnet-genesis-2-node> --sha256 <its sha256> \
|
||||||
|
--miner <the seed-class igneum-miner> --digest <the FINAL digest the identity test pins> --wipe-genesis --genesis 01294fd322704dc28fbef0e7a5ef86d6ee260ac5efaf88891cdba661b5fd58ac
|
||||||
|
infra/build-server/wave1-0320.sh seeds ... --go # the same line with --go, on the project lead's word
|
||||||
|
```
|
||||||
|
|
||||||
|
What it does with `--go`, on seed1, seed2 and seed3 in parallel: the binary put as `/opt/igneum/bin/igneumd.new` with the sha asserted on
|
||||||
|
the box, the unit stopped, `/var/lib/igneum/igneum-testnet-1` moved aside as `igneum-testnet-1.prev-<UTC stamp>` (kept), the binary swapped
|
||||||
|
(the old kept as `igneumd.prev`), the unit started, then one RESULT line per seed: unit state and downtime, `genesis <hash> MATCH` from the
|
||||||
|
`[igneum-exec] genesis ... executed` line against `--genesis`, the commit string count in the installed binary, the digest line against
|
||||||
|
`--digest`, `eth_syncing` and `net_peerCount` over the loopback RPC, the first journal line, and "base unit 10^18 line seen" (the 5 October binary never prints it, the re-cut always does). No `--override`: the testnet takes none. The
|
||||||
|
dry run of 15:1x UK read all three seeds on 1c19441d at height 0 (`active a9ea25f8ada2`). The digest passed as `--digest` is the one the
|
||||||
|
final object prints (`63faee44...`), never a value from memory.
|
||||||
|
|
||||||
|
The route the shipper staged (0.3.20 cut, main's ruling (b), 14:4x UK, `docs/plans/release-0.3.20.md` 5e74fa2f): the three seeds stay
|
||||||
|
on 1c19441d (digest `b7d8c915...`) through the 0.3.20 sweep and the public RPC filter's BLOCKED_UNTIL_FIXED_NODE set stays; at the go the
|
||||||
|
build-server lane's `wave1-0320.sh` seeds mode puts the re-cut binary on the three seeds in parallel (the sha asserted, the commit string
|
||||||
|
and the digest read back) and its lift-rpc-filter mode opens the RPC; steps 2 to 4 above are what that mode does, step 2's wipe included.
|
||||||
|
|
||||||
|
The digest lineage, so the move is read right at the go: 1c19441d `b7d8c915...` (5 October); the 0.3.20 pin c4459193 `9537868d...`, the
|
||||||
|
whole move being five additions (`finality_leave_activation_daa` 0 with `finality.leave_delay` 3,600, `program_class_v3_activation_daa` 0,
|
||||||
|
`program_class_v4_activation_daa` 0 with the signal window 0 left out, `pow_genesis_dataset_log2` 28, `EmissionSchedule::TESTNET_1`), the
|
||||||
|
genesis, base params, finality table, fee table and pow schedule unchanged; this re-cut `80af8aa1...` on top of those: `base_unit_decimals`
|
||||||
|
18 (and the genesis itself, re-laid), `difficulty_v3_activation_daa` 0, `finality_daa_rule_activation_daa` 0, the signing bonus 0 at 1,000 bps,
|
||||||
|
`latency_ladder_activation_daa` 0 with the window and the six rungs, `proving_consensus_verify_daa` 0 with the two program ids,
|
||||||
|
`subsidy_per_block_activation_daa` 0; then `4fbb2152...` with the genesis-forward fields (`sig_scheme_activation_daa` 0 with `sig_scheme` 0, `finality_succession_activation_daa` 0, the cache rung in the ladder arm); then `63faee44...` with the cache rung behind its own switch (`latency_ladder_cache_rung_activation_daa` 0, a fourth arm), the final object. No override pins any old value, so every testnet node
|
||||||
|
swaps together or not at all; the digest is re-read on the re-cut's own binary at the go.
|
||||||
|
|
||||||
|
What the project lead's go needs from him, in order: (a) nothing on item 8 any more (it is in); (b) "cut the seeds over" (this runbook, about 20 minutes for the three); (c) the 0.4.0 cut from the merged branches (step 3 of the go table); (d) the first miner. Until (b) the seeds keep the 5 October chain and refuse every re-cut node, which is harmless: nothing mines on either.
|
||||||
|
|
||||||
## Cost
|
## Cost
|
||||||
|
|
||||||
| Item | USD per month, net (Hetzner API, 5 October 2026) |
|
| Item | USD per month, net (Hetzner API, 5 October 2026) |
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Igneum public testnet: identity, parameters and reset policy (ADOPTED 5 October 2026)
|
# Igneum public testnet: identity, parameters and reset policy (ADOPTED 5 October 2026; genesis RE-CUT 7 October 2026; the GO object re-cut on the 0.3.23 line at 18 decimals by the project lead's word of 21:35 BST)
|
||||||
|
|
||||||
Every value in this file was proposed on the night of 4 October 2026 and ADOPTED by the owner on 5 October 2026,
|
Every value in this file was proposed on the night of 4 October 2026 and ADOPTED by the owner on 5 October 2026,
|
||||||
as proposed (sign-off recorded in `docs/plans/release-0.3.6.md`). The genesis below is the one the proposal
|
as proposed (sign-off recorded in `docs/plans/release-0.3.6.md`). The genesis below is the one the proposal
|
||||||
|
|
@ -8,7 +8,13 @@ merged on 5 October 2026 into the fork's `release-0.3.6` (worktree `vendor/igneu
|
||||||
sign-off added: the devnet and the simnet keep the prototype fee set until a height switch
|
sign-off added: the devnet and the simnet keep the prototype fee set until a height switch
|
||||||
(`fees_v1_activation_daa`) or a `fees` object in the override file moves them; the testnet and the mainnet carry
|
(`fees_v1_activation_daa`) or a `fees` object in the override file moves them; the testnet and the mainnet carry
|
||||||
calibrated v1 from genesis (section 3). The devnet's switch is DAA score 210,000 (5 October 2026,
|
calibrated v1 from genesis (section 3). The devnet's switch is DAA score 210,000 (5 October 2026,
|
||||||
`docs/plans/fee-switch-devnet.md`). The three seed nodes went up on 5 October 2026 from the final genesis below (`docs/plans/testnet-go.md`); nothing mines until the owner's go.
|
`docs/plans/fee-switch-devnet.md`). The three seed nodes went up on 5 October 2026 from the 5 October genesis (`docs/plans/testnet-go.md`); nothing mines until the owner's go.
|
||||||
|
|
||||||
|
RE-CUT 7 October 2026 (the project lead's approvals of 09:3x UK, `docs/plans/ledger-decisions.md` "Decisions (7 October 2026, 09:3x UK)"):
|
||||||
|
one cut with 18 decimals (O-2.6, the 16-byte coinbase subsidy), `EmissionSchedule::TESTNET_1`, and every switch on from
|
||||||
|
genesis. The genesis hash and the consensus digest below are the re-cut's; the seeds still hold the 5 October chain at
|
||||||
|
height 0 and move to the re-cut binary only on the project lead's go (`docs/plans/testnet-go.md`, the cut-over runbook). Fork branch
|
||||||
|
`testnet-genesis-2-node` (release-0.3.18-node e69e8a39, the decimals branch df2fbd03, the vote-or-burn removal 420f9305).
|
||||||
|
|
||||||
## 1. Identity
|
## 1. Identity
|
||||||
|
|
||||||
|
|
@ -40,22 +46,33 @@ read methods plus `eth_sendRawTransaction`; `infra/seed-nodes/rpc/`).
|
||||||
| Nonce, DAA score | 0, 0 | |
|
| Nonce, DAA score | 0, 0 | |
|
||||||
| UTXO commitment | empty | |
|
| UTXO commitment | empty | |
|
||||||
| Coinbase payload message | `igneum-testnet-1 \| 2026-10-05 \| coins here have no value \| resets are announced` | after the OP-FALSE script, as the devnet's `igneum-devnet`. FINAL 5 October 2026 (fork branch `testnet-infra`, 1c19441d): the proposal's "proposed, not final" was dropped before the first public node started, as `docs/plans/release-0.3.6.md` section 6 required. The message never changes again on `igneum-testnet-1` |
|
| Coinbase payload message | `igneum-testnet-1 \| 2026-10-05 \| coins here have no value \| resets are announced` | after the OP-FALSE script, as the devnet's `igneum-devnet`. FINAL 5 October 2026 (fork branch `testnet-infra`, 1c19441d): the proposal's "proposed, not final" was dropped before the first public node started, as `docs/plans/release-0.3.6.md` section 6 required. The message never changes again on `igneum-testnet-1` |
|
||||||
| Hash | `87617621714af1bf33bd17f291f90a7e0bff760a669bba53083ea8c0f7cbd840` | computed 5 October 2026 by `print_genesis_hashes` (two runs: the merkle root first, then the header hash over it), pinned by `test_genesis_hashes` and `igneum_testnet_identity`; the three seeds started from it at height 0 the same day. The proposal's hash `52a3e6a9...` is void |
|
| Coinbase payload subsidy field | 16 little-endian bytes, one IGN = 10^18 base units | the 18-decimal layout (O-2.6, `docs/design/base-unit.md` section 3); block 1 merges the genesis and reads this field at the network's unit |
|
||||||
| Merkle root | `44acfc40b1c6647011510f3c39ddb7606f979df92ad26a2914de56e44610efad` | same |
|
| Hash | `01294fd322704dc28fbef0e7a5ef86d6ee260ac5efaf88891cdba661b5fd58ac` | RE-CUT 7 October 2026, computed on igneum-build-1 by `print_genesis_hashes` (two runs: the merkle root first, then the header hash over it), pinned by `test_genesis_hashes` and `igneum_testnet_identity`. The 5 October hash `87617621...` (8-byte subsidy layout, the chain the seeds hold at height 0 until the cut-over) and the proposals `52a3e6a9...` and `494fc9a3...` are void |
|
||||||
|
| Merkle root | `bcd0e8fb1099aeb3cc1370b50e6dec9f615e204374b2097f25b2438e7d22f49f` | same (5 October: `44acfc40...`) |
|
||||||
|
| Consensus digest | `63faee44f50eccd3d68c970efbc39edee2977a4f0fbf4baa833aa5ef5de3577a` | exchanged in the p2p handshake; a peer with another digest is refused. Pinned by `igneum_testnet_identity`; the whole object as override-file JSON is `infra/seed-nodes/testnet-object.json` (`print_testnet_object`). The 5 October digest `b7d8c915...` and the re-cut's interim digests `9390d235...`, `80af8aa1...` and `4fbb2152...` are void. The devnet's `c562d70e...` is unchanged |
|
||||||
|
|
||||||
## 3. Consensus parameters
|
## 3. Consensus parameters
|
||||||
|
|
||||||
| Parameter | Testnet (adopted) | Devnet today | Why |
|
| Parameter | Testnet (adopted) | Devnet today | Why |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Block rate | 1 per second | 1 per second | spec 02 |
|
| Block rate | 1 per second | 1 per second | spec 02 |
|
||||||
| Difficulty rule | Igneum dual-lane, v2 from genesis | dual-lane, v2 from DAA 33,000 | a fresh chain has no pre-switch history |
|
| Difficulty rule | Igneum dual-lane, v2 and v3 from genesis (`difficulty_v3_activation_daa` 0) | dual-lane, v2 from DAA 33,000, v3 by the override file | a fresh chain has no pre-switch history |
|
||||||
| Finality parameters | `FinalityParams::MAINNET`: 30-day weight window (2,592,000 DAA), dust 100, presence 240, 8 aggregators, 30-day equivocation ban, min DAA 2,592,000, certificate fold 6 | `DEVNET`: 2-hour window, dust 5, presence 20, fold 3 | the testnet runs the rule the mainnet will run; the first lock needs 30 days of weight, which is the point of a testnet |
|
| Finality parameters | `FinalityParams::MAINNET`: 30-day weight window (2,592,000 DAA), dust 100, presence 240, 8 aggregators, 30-day equivocation ban, min DAA 2,592,000, certificate fold 6, leave delay 3,600 | `DEVNET`: 2-hour window, dust 5, presence 20, fold 3 | the testnet runs the rule the mainnet will run; the first lock needs 30 days of weight, which is the point of a testnet |
|
||||||
| Finality rule v3 | from genesis | from the override file | fresh chain |
|
| Finality rule v3 | from genesis | from the override file | fresh chain |
|
||||||
|
| The DAA-second finality rule (C1) | from genesis (`finality_daa_rule_activation_daa` 0): a checkpoint every 30 s of chain time at any block rate | by the override file | re-cut 7 October 2026 |
|
||||||
|
| The signed leave item (W7) | from genesis (`finality_leave_activation_daa` 0, delay 3,600 DAA s): a key that announces its departure is in no denominator an hour later | never until the 95 percent signal | the project lead, 6 October 2026, question 4 |
|
||||||
|
| The signing bonus, no burn | from genesis (`signing_bonus_activation_daa` 0, `signing_bonus_bps` 1,000): a tenth of a silent producer's subsidy share moves to the proving pool; nothing is ever destroyed; vote-or-burn is out of the tree (420f9305) | never | the project lead, 7 October 2026, row 2 |
|
||||||
|
| Consensus proof verification | from genesis (`proving_consensus_verify_daa` 0) under the shard program id `0x2b1a81cb...ef7a` and the aggregator id `0x474678f3...3896` of `proving/igneum-prove/elf/manifest.json` (pinned 2026-10-05T16:20:38Z); a node whose embedded keys differ refuses to start; a Windows node verifies through the installed `igneum-prove-host` and refuses to start without it | off | the project lead, 6 October 2026, question 3 |
|
||||||
|
| Program class | v4 from genesis, unconditional (`program_class_v4_activation_daa` 0, signal window 0) | v4 by miner signal | fresh chain |
|
||||||
|
| The latency ladder | active from genesis at rung 0 (`latency_ladder_activation_daa` 0, window 86,400 DAA, seven windows at 90 percent to step): rungs 27, 35, 53 passes admissible; 88 inadmissible (quiet-core re-measure 7 October 2026, 08:46 UK: 10.85 ms cold with the SMT sibling loaded, over the 10 ms gate); 173 and 267 inadmissible | never | the project lead, 7 October 2026, row 3 |
|
||||||
|
| Base unit | 18 decimals (`base_unit_decimals` 18): one IGN is 10^18 base units, the EVM's wei, the bridge is the identity | 8 (sompi) | the project lead, 6 October 2026, question 5, CONFIRMED 7 October 2026, 21:35 BST for the go object on the 0.3.23 line; `docs/design/base-unit.md` |
|
||||||
|
| Per-block subsidy on the EVM side | from genesis (`subsidy_per_block_activation_daa` 0): each merged block credited the subsidy of its own DAA, as the UTXO coinbase pays it | at an upgrade height with 0.3.19 | the project lead, 7 October 2026, 09:5x UK |
|
||||||
|
| Genesis forward-compatibility (mission item 8) | from genesis: `sig_scheme` 0 (BLS12-381) with `sig_scheme_activation_daa` 0 (every vote item and key reveal carries the scheme byte; any other scheme refused until a program class the 95 percent signal moves to names it), `finality_succession_activation_daa` 0 (W5: a vote key hands its window weight and forfeit term to a successor once), the ladder's cache rung 512 MiB inadmissible until measured, behind its own switch (`latency_ladder_cache_rung_activation_daa` 0) | never | the project lead, 7 October 2026, 10:1x UK; `docs/design/genesis-forward.md` |
|
||||||
| Proving v0 payouts | from genesis | from the override file | fresh chain |
|
| Proving v0 payouts | from genesis | from the override file | fresh chain |
|
||||||
| PoW schedule | epoch 3,600 DAA, lead 600, day 86,400,000 ms (the defaults) | same | |
|
| PoW schedule | epoch 3,600 DAA, lead 600, day 86,400,000 ms (the defaults) | same | |
|
||||||
| Coinbase payload limit | 16,384 (the finality section) | same | |
|
| Coinbase payload limit | 16,384 (the finality section) | same | |
|
||||||
| Fees | `FeeParams::CALIBRATED_V1` from genesis (`fees_v1_activation_daa` 0; `docs/analysis/base-fee-floor.md`): `B_p` 120,000 pgas, `S_p` 30,000, intrinsic 300, modexp 10 + 1 per 10 bytes, floors 100 gwei per gas and 10,000 gwei per pgas | `FeeParams::PROTOTYPE` (`B_p` 30 M, 1 gwei), kept on the 0.3.6 node so the live chain does not change rules between builds; moves to v1 by the `fees_v1_activation_daa` height switch in the override file (`docs/plans/release-0.3.6.md`, section 5) | spec 05 section 5.10 |
|
| Fees | `FeeParams::CALIBRATED_V1` from genesis (`fees_v1_activation_daa` 0; `docs/analysis/base-fee-floor.md`): `B_p` 120,000 pgas, `S_p` 30,000, intrinsic 300, modexp 10 + 1 per 10 bytes, floors 100 gwei per gas and 10,000 gwei per pgas | `FeeParams::PROTOTYPE` (`B_p` 30 M, 1 gwei), kept on the 0.3.6 node so the live chain does not change rules between builds; moves to v1 by the `fees_v1_activation_daa` height switch in the override file (`docs/plans/release-0.3.6.md`, section 5) | spec 05 section 5.10 |
|
||||||
| Emission | the mainnet schedule: 31.69 IGN per block in year one, halving every two years, cap 4 billion | same | the testnet coins have no value whatever the schedule says |
|
| Emission | `EmissionSchedule::TESTNET_1` at 18 decimals: 100 IGN a block at 1 bps, a monthly glide with a two-year half-life, a 90-day ramp from 10 percent, a tail of 1 percent of supply a year from the month the glide first pays under it (about year 11.4), no hard cap (`docs/analysis/tail-emission.md`) | `CURRENT`: 31.69 IGN per block in year one, halving every two years, cap 4 billion | the project lead, 7 October 2026, row 1; the testnet coins have no value whatever the schedule says |
|
||||||
|
|
||||||
Everything else (mass limits, GHOSTDAG k, merge depth, pruning) is the devnet's set, unchanged.
|
Everything else (mass limits, GHOSTDAG k, merge depth, pruning) is the devnet's set, unchanged.
|
||||||
|
|
||||||
|
|
@ -81,5 +98,6 @@ Everything else (mass limits, GHOSTDAG k, merge depth, pruning) is the devnet's
|
||||||
| The public RPC and explorer | the RPC is `https://rpc.testnet.igneum.network` (5 October 2026); the explorer is not built |
|
| The public RPC and explorer | the RPC is `https://rpc.testnet.igneum.network` (5 October 2026); the explorer is not built |
|
||||||
| The prover's table mirror and fixtures | `docs/analysis/base-fee-floor.md` section 4 |
|
| The prover's table mirror and fixtures | `docs/analysis/base-fee-floor.md` section 4 |
|
||||||
| The app's testnet build | branch `testnet-app` (5 October 2026): the packaged file's `network`, `peers`, `public_rpc`; the testnet's ports, seeds and node directory in `app/igneum-app/src/config.rs`; the release engineer cuts 0.4.0 from it at go |
|
| The app's testnet build | branch `testnet-app` (5 October 2026): the packaged file's `network`, `peers`, `public_rpc`; the testnet's ports, seeds and node directory in `app/igneum-app/src/config.rs`; the release engineer cuts 0.4.0 from it at go |
|
||||||
| The params digest in the handshake (X18) | separate work, before the testnet |
|
| The params digest in the handshake (X18) | done: `9390d235...` for the re-cut object |
|
||||||
|
| The seeds on the re-cut genesis | NOT DONE: the three seeds hold the 5 October chain at height 0; the cut-over (new binary, wiped data directory, the digest line read back on each) runs on the project lead's go, `docs/plans/testnet-go.md` |
|
||||||
| Terms on the download page | `site/index.html#testnet-terms`, on branch `testnet-prep` with this file |
|
| Terms on the download page | `site/index.html#testnet-terms`, on branch `testnet-prep` with this file |
|
||||||
|
|
|
||||||
452
infra/fast-time/testnet-object.mjs
Normal file
452
infra/fast-time/testnet-object.mjs
Normal file
|
|
@ -0,0 +1,452 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// igneum-testnet-1's object at fast time (the testnet genesis lane, 7 October 2026): a 3-node devnet-suffix network on
|
||||||
|
// override-60x.json with every switch of infra/seed-nodes/testnet-object.json on from genesis, as the testnet carries
|
||||||
|
// them (difficulty v3, the DAA-second finality rule, finality v3, the signed leave item, the signing bonus at 1,000 bps,
|
||||||
|
// class v4 unconditional, the latency ladder active at rung 0, consensus proof verification under the pinned ids, fees
|
||||||
|
// v1, EmissionSchedule::TESTNET_1), real CPU mining on every node (igneum-pow engine, CPU genesis bits), four voting keys
|
||||||
|
// and one key that never votes (--no-vote). What the testnet itself cannot show inside a run (its weight window is 30
|
||||||
|
// days) this run shows in minutes: the first lock, a silent producer paid the bonus split, and a signed leave.
|
||||||
|
//
|
||||||
|
// What stays the devnet's, by design: the base unit (8 decimals: the devnet genesis payload is 8 bytes and the daemon
|
||||||
|
// refuses a unit its genesis does not parse at; the 18-decimal form is tools/fleet/base-unit-gate.sh on the testnet
|
||||||
|
// params), the finality object (the 60x file's: window 120, min_daa 120, presence 1, leave delay 60 = 3,600 / 60), the
|
||||||
|
// ladder window (60 DAA, one epoch, in place of 86,400) and the PoW schedule (epoch 60, lead 10, day 24 minutes).
|
||||||
|
// The testnet schedule is written at the devnet's unit (100 IGN a block = 10^10 sompi).
|
||||||
|
//
|
||||||
|
// Ports 29730 and up, network igneum-devnet-973, data under IGNEUM_TN_TMP (default /tmp/igneum-fast-time-testnet). The
|
||||||
|
// live devnet, Devnet 2 and the public testnet seeds are never touched (--nodnsseed, loopback only). Everything started
|
||||||
|
// is stopped at the end, by pid, never by name.
|
||||||
|
//
|
||||||
|
// Checks (each a line in the summary; PASS is every one true):
|
||||||
|
// every node prints the start-up lines of the object (digest, class v4, ladder active, proof verification from 0, fees v1,
|
||||||
|
// the signing bonus from 0, the leave item from 0)
|
||||||
|
// every epoch's template is class v4 at rung 0 (27 shadow passes); no ladder bits on the chain (no node signals); the
|
||||||
|
// object byte of every mined block is 0; the sinks and block counts agree across the nodes
|
||||||
|
// the first finality lock arrives (finality_active, latest_locked_index > 0) on every node
|
||||||
|
// the bonus pays: after the first lock, a chain block whose mergeset holds a block of the silent key pays that key's
|
||||||
|
// address 72 percent of the merged block's subsidy on the UTXO side and the pool 28 percent (the voting keys 80/20)
|
||||||
|
// the bridge is the identity under the bonus: for every chain block after the first lock, the sum of the UTXO coinbase
|
||||||
|
// outputs to each miner's address, times 10^10, equals the sum of the execution layer's segment rewards to that
|
||||||
|
// miner's EVM address (igneum_getSegment). Before the exec-side fix of 7 October 2026 this is the known-failed case:
|
||||||
|
// the executor credits 80 percent to a silent key while the coinbase pays 72 (the testnet lane's finding, 08:0x UK)
|
||||||
|
// a leave: at --leave-at seconds a voting key submits its signed leave (igneum-miner leave); the node accepts it, and
|
||||||
|
// within leave_delay + one checkpoint the checkpoints' voter count drops by one
|
||||||
|
//
|
||||||
|
// node infra/fast-time/testnet-object.mjs [--secs 600] [--leave-at 360] [--expect pass|bonus-fails]
|
||||||
|
// IGNEUMD, IGNEUM_MINER, IGNEUM_POW name the binaries (defaults: the testnet genesis worktree's fork under
|
||||||
|
// vendor/igneum-node-testnet-genesis/target/release and igneum-pow/target/release/igneum-pow, the layout on
|
||||||
|
// igneum-build-1 under /srv/builds/igneum-wt-testnet-genesis).
|
||||||
|
|
||||||
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
||||||
|
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
|
||||||
|
import { devAddress } from '../../tools/harness/lib/address.mjs';
|
||||||
|
|
||||||
|
const ROOT = new URL('../../', import.meta.url).pathname;
|
||||||
|
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
|
||||||
|
const OBJECT = `${ROOT}infra/seed-nodes/testnet-object.json`;
|
||||||
|
const BIN = process.env.IGNEUM_TN_BIN || `${ROOT}vendor/igneum-node-testnet-genesis/target/release`;
|
||||||
|
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
||||||
|
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
||||||
|
const IGNEUM_POW = process.env.IGNEUM_POW || `${ROOT}igneum-pow/target/release/igneum-pow`;
|
||||||
|
const TMP = process.env.IGNEUM_TN_TMP || '/tmp/igneum-fast-time-testnet';
|
||||||
|
const BASE = +(process.env.IGNEUM_TN_BASE_PORT || 29730), SUFFIX = +(process.env.IGNEUM_TN_SUFFIX || 973);
|
||||||
|
const RUNG0 = 27, SOMPI = 10n ** 8n, WEI_PER_SOMPI = 10n ** 10n;
|
||||||
|
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', 600);
|
||||||
|
const LEAVE_AT = flag('leave-at', 360);
|
||||||
|
const EXPECT = sflag('expect') || 'pass';
|
||||||
|
if (!['pass', 'bonus-fails'].includes(EXPECT)) { console.error('usage: [--secs 600] [--leave-at 360] [--expect pass|bonus-fails]'); 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); }
|
||||||
|
if (!existsSync(OBJECT)) { console.error(`missing ${OBJECT} (print_testnet_object writes it)`); process.exit(2); }
|
||||||
|
|
||||||
|
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
||||||
|
// u64::MAX is not a JavaScript number: the files are merged as text, never through JSON.parse of the whole object
|
||||||
|
const baseText = readFileSync(FILE, 'utf8');
|
||||||
|
const objectText = readFileSync(OBJECT, 'utf8');
|
||||||
|
const num = (text, name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(text); return m ? m[1] : undefined; };
|
||||||
|
const str = (text, name) => { const m = new RegExp(`"${name}":\\s*"([^"]*)"`).exec(text); return m ? m[1] : undefined; };
|
||||||
|
const EPOCH = +num(baseText, 'pow_epoch_blocks'), LEAD = +num(baseText, 'pow_epoch_lead'), DAY_MS = +num(baseText, 'pow_day_ms');
|
||||||
|
const LEAVE_DELAY = 60, LADDER_WINDOW = 60;
|
||||||
|
// the switches as the object carries them, the clocks at fast time, the unit the devnet's
|
||||||
|
const switches = {
|
||||||
|
genesis_bits: String(GENESIS_BITS), skip_proof_of_work: 'false',
|
||||||
|
difficulty_v2_activation_daa: num(objectText, 'difficulty_v2_activation_daa'),
|
||||||
|
difficulty_v3_activation_daa: num(objectText, 'difficulty_v3_activation_daa'),
|
||||||
|
proving_v0_activation_daa: num(objectText, 'proving_v0_activation_daa'),
|
||||||
|
finality_v3_activation_daa: num(objectText, 'finality_v3_activation_daa'),
|
||||||
|
finality_daa_rule_activation_daa: num(objectText, 'finality_daa_rule_activation_daa'),
|
||||||
|
finality_leave_activation_daa: num(objectText, 'finality_leave_activation_daa'),
|
||||||
|
signing_bonus_activation_daa: num(objectText, 'signing_bonus_activation_daa'),
|
||||||
|
signing_bonus_bps: num(objectText, 'signing_bonus_bps'),
|
||||||
|
program_class_v3_activation_daa: num(objectText, 'program_class_v3_activation_daa'),
|
||||||
|
program_class_v4_activation_daa: num(objectText, 'program_class_v4_activation_daa'),
|
||||||
|
program_class_v4_signal_window_daa: num(objectText, 'program_class_v4_signal_window_daa'),
|
||||||
|
latency_ladder_activation_daa: num(objectText, 'latency_ladder_activation_daa'),
|
||||||
|
latency_ladder_window_daa: String(LADDER_WINDOW),
|
||||||
|
fees_v1_activation_daa: num(objectText, 'fees_v1_activation_daa'),
|
||||||
|
proving_consensus_verify_daa: num(objectText, 'proving_consensus_verify_daa'),
|
||||||
|
subsidy_per_block_activation_daa: num(objectText, 'subsidy_per_block_activation_daa'),
|
||||||
|
proving_shard_program_id: JSON.stringify(str(objectText, 'proving_shard_program_id')),
|
||||||
|
proving_aggregator_id: JSON.stringify(str(objectText, 'proving_aggregator_id')),
|
||||||
|
};
|
||||||
|
for (const [k, v] of Object.entries(switches)) if (v === undefined) { console.error(`the object has no ${k}`); process.exit(2); }
|
||||||
|
const ladderList = /"latency_ladder":\s*(\[[\s\S]*?\])/.exec(objectText)[1].replace(/\s+/g, '');
|
||||||
|
const feesObject = /"fees":\s*(\{[\s\S]*?\n \})/.exec(objectText)[1].replace(/\s+/g, '');
|
||||||
|
const emissionObject = /"emission":\s*(\{[\s\S]*?\n \})/.exec(objectText)[1].replace(/\s+/g, '').replace(/"launch_rate":"(\d+)"/, (_, r) => `"launch_rate":"${BigInt(r) / WEI_PER_SOMPI}"`);
|
||||||
|
// the value of a top-level key in the file's text, nested objects and lists included (brace matching, never
|
||||||
|
// JSON.parse: u64::MAX is not a JavaScript number)
|
||||||
|
function spanOf(text, key) {
|
||||||
|
const m = new RegExp(`"${key}":\\s*`).exec(text);
|
||||||
|
if (!m) return null;
|
||||||
|
const start = m.index; let i = m.index + m[0].length;
|
||||||
|
const c = text[i];
|
||||||
|
if (c === '{' || c === '[') {
|
||||||
|
const open = c, close = c === '{' ? '}' : ']'; let depth = 0;
|
||||||
|
for (; i < text.length; i++) { if (text[i] === open) depth++; else if (text[i] === close) { depth--; if (depth === 0) { i++; break; } } }
|
||||||
|
} else if (c === '"') { i = text.indexOf('"', i + 1) + 1; } else { while (i < text.length && !/[,}\n]/.test(text[i])) i++; }
|
||||||
|
// the trailing comma and the line break before the key
|
||||||
|
if (text[i] === ',') i++;
|
||||||
|
let s = start; while (s > 0 && /\s/.test(text[s - 1])) s--;
|
||||||
|
return [s, i];
|
||||||
|
}
|
||||||
|
function mergeOverrideText(text, fields) {
|
||||||
|
let out = text;
|
||||||
|
for (const k of Object.keys(fields)) { const span = spanOf(out, k); if (span) out = out.slice(0, span[0]) + out.slice(span[1]); }
|
||||||
|
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${v}`).join(',\n ');
|
||||||
|
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
|
||||||
|
}
|
||||||
|
// the finality object: the 60x file's, with the leave delay at fast time
|
||||||
|
const finSpan = spanOf(baseText, 'finality');
|
||||||
|
const fastFinality = baseText.slice(finSpan[0], finSpan[1]).replace(/^[\s,]*"finality":\s*/, '').replace(/,\s*$/, '').replace(/\s+/g, '');
|
||||||
|
const finalityObject = fastFinality.includes('leave_delay') ? fastFinality.replace(/"leave_delay":\d+/, `"leave_delay":${LEAVE_DELAY}`) : fastFinality.replace(/\}$/, `,"leave_delay":${LEAVE_DELAY}}`);
|
||||||
|
const override = `${TMP}/override.json`;
|
||||||
|
writeFileSync(override, mergeOverrideText(baseText, { ...switches, latency_ladder: ladderList, fees: feesObject, emission: emissionObject, finality: finalityObject }));
|
||||||
|
log(`object: ${OBJECT}; every switch from genesis; finality ${finalityObject}; ladder window ${LADDER_WINDOW}; emission ${emissionObject}; run ${SECS} s, leave at ${LEAVE_AT} s; expect ${EXPECT}`);
|
||||||
|
|
||||||
|
const LABELS = ['tn-voter-a', 'tn-voter-b', 'tn-voter-c', 'tn-voter-d', 'tn-silent'];
|
||||||
|
const EVM = ['00000000000000000000000000000000000000a0', '00000000000000000000000000000000000000a1', '00000000000000000000000000000000000000a2', '00000000000000000000000000000000000000a3', '00000000000000000000000000000000000000a4'];
|
||||||
|
const N = LABELS.length, SILENT = N - 1, LEAVER = 1;
|
||||||
|
// each miner's payout address as the miner itself prints it (its first log line names it); devAddress(label) is the
|
||||||
|
// harness's own derivation and is not the miner's, so the chain's outputs are matched against the printed one
|
||||||
|
let ADDR = LABELS.map(l => devAddress(l));
|
||||||
|
function addressesFromMinerLogs() {
|
||||||
|
return LABELS.map((_, i) => { const m = minerLog(i).join('\n').match(/igneum(?:dev|test)?:[a-z0-9]{20,}/); return m ? m[0] : ADDR[i]; });
|
||||||
|
}
|
||||||
|
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.evmPort = BASE + i * 10 + 3;
|
||||||
|
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}`, `--evm-rpclisten=127.0.0.1:${this.evmPort}`,
|
||||||
|
`--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);
|
||||||
|
writeFileSync(`${TMP}/n${this.i}.pid`, String(this.proc.pid));
|
||||||
|
await sleep(1500);
|
||||||
|
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
|
||||||
|
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort} evm ${this.evmPort}`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
|
||||||
|
async evm(method, params) {
|
||||||
|
const r = await fetch(`http://127.0.0.1:${this.evmPort}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
|
||||||
|
return (await r.json()).result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function miner(bin, argv, name, env = {}) {
|
||||||
|
const out = openSync(`${TMP}/${name}.log`, 'a');
|
||||||
|
const p = spawn(bin, argv, { stdio: ['ignore', out, out], env: { ...process.env, ...env } });
|
||||||
|
started.push(p);
|
||||||
|
writeFileSync(`${TMP}/${name}.pid`, String(p.pid));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
async function stopAll() {
|
||||||
|
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
|
||||||
|
await sleep(1500);
|
||||||
|
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
|
||||||
|
}
|
||||||
|
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
|
||||||
|
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
|
||||||
|
process.on('uncaughtException', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
|
||||||
|
const minerLog = (i) => { try { return readFileSync(`${TMP}/cpu${i}.log`, 'utf8').split('\n'); } catch { return []; } };
|
||||||
|
|
||||||
|
const t0 = Date.now();
|
||||||
|
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
|
||||||
|
try {
|
||||||
|
const n0 = await new Node(0).start();
|
||||||
|
const nodes = [n0];
|
||||||
|
for (let i = 1; i < N; i++) nodes.push(await new Node(i, [`127.0.0.1:${n0.p2pPort}`]).start());
|
||||||
|
const n1 = nodes[LEAVER];
|
||||||
|
const START_LINES = {
|
||||||
|
digest: /Consensus params digest: ([0-9a-f]{64})/,
|
||||||
|
class_v4: /program class v4|class v4 from|Program class: v4|class_v4/i,
|
||||||
|
ladder_active: /Latency ladder active: rungs/,
|
||||||
|
proof_verification_from_0: /Proving: consensus proof verification from DAA score 0/,
|
||||||
|
fees_v1: /calibrated v1 from DAA score 0/,
|
||||||
|
finality_v3_and_c1: /rule v3 \(frozen table, certificate fold\) from checkpoint DAA 0.*C1 in DAA seconds/,
|
||||||
|
};
|
||||||
|
for (const n of nodes) log(`n${n.i}: ${Object.entries(START_LINES).map(([k, re]) => `${k}=${n.grepLog(re).length > 0}`).join(' ')} | digest ${(n.grepLog(START_LINES.digest)[0] || '').replace(/^.*?digest: /, '').slice(0, 16)}`);
|
||||||
|
// n0 and n1 vote (the key is the payout label's identity); n2 never votes: the silent key
|
||||||
|
const miners = nodes.map((n, i) => miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', LABELS[i], '--evm-address', EVM[i], '--dev-fee', '0', '--status-secs', '30', '--stall-secs', '0', ...(i === SILENT ? ['--no-vote'] : [])], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) }));
|
||||||
|
const pay = devAddress('fast-time-testnet-object');
|
||||||
|
|
||||||
|
const epochs = new Map();
|
||||||
|
let lastEpoch = -1, lastReport = 0, lastDaa = 0, firstLock = null, leaveSent = null, leaveOutcome = null, votersBeforeLeave = null, votersAfterLeave = null, leaveSeenAt = null;
|
||||||
|
const samples = [];
|
||||||
|
const inactiveAfterLeave = [];
|
||||||
|
async function report(node, last = 50) { return node.rpc.call('getFinalityCheckpoints', { last }).catch(() => null); }
|
||||||
|
while (Date.now() - t0 < SECS * 1000) {
|
||||||
|
await sleep(1000);
|
||||||
|
let daa = null, epoch = null, cls = null, reps = null, step = null, sig = null;
|
||||||
|
try {
|
||||||
|
const t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
|
||||||
|
const pe = t.powEpoch || t.pow_epoch || {};
|
||||||
|
daa = pe.virtualDaaScore ?? t.block?.header?.daaScore; epoch = pe.epochIndex; cls = pe.programClass;
|
||||||
|
reps = pe.latencyLadderReps; step = pe.latencyLadderStep; sig = pe.latencyLadderSignal;
|
||||||
|
} catch (e) { log(`template: ${e.message}`); }
|
||||||
|
if (epoch != null && epoch !== lastEpoch) {
|
||||||
|
epochs.set(epoch, { class: cls, reps, step, firstSeenDaa: daa, at: +since() });
|
||||||
|
log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s: template class ${cls} rung ${step} (${reps} passes), this node signals ${sig}`);
|
||||||
|
lastEpoch = epoch;
|
||||||
|
}
|
||||||
|
lastDaa = daa ?? lastDaa;
|
||||||
|
const r = await report(n0, 5);
|
||||||
|
if (r && firstLock == null && r.latestLockedIndex > 0) { firstLock = { index: r.latestLockedIndex, daa, at: +since() }; log(`FIRST LOCK: index ${r.latestLockedIndex} at daa ${daa}, ${since()} s wall (active ${r.finalityActive})`); }
|
||||||
|
if (leaveSent == null && Date.now() - t0 >= LEAVE_AT * 1000 && firstLock != null) {
|
||||||
|
const before = await report(n1, 3);
|
||||||
|
votersBeforeLeave = before?.checkpoints?.at(-1)?.voters ?? null;
|
||||||
|
const out = spawnSync(CPU_MINER, ['leave', n1.grpc, LABELS[LEAVER]], { encoding: 'utf8', timeout: 20000 });
|
||||||
|
leaveSent = { at: +since(), daa, stdout: (out.stdout || '').trim().slice(0, 300), stderr: (out.stderr || '').trim().slice(0, 300) };
|
||||||
|
leaveOutcome = /LEAVE key=/.test(out.stdout || '') ? 'accepted' : 'refused';
|
||||||
|
log(`LEAVE by ${LABELS[LEAVER]} at daa ${daa}: ${leaveOutcome} (${leaveSent.stdout || leaveSent.stderr}); voters before ${votersBeforeLeave}; stopping its miner (a clean departure)`);
|
||||||
|
try { miners[LEAVER].kill('SIGINT'); } catch { }
|
||||||
|
}
|
||||||
|
if (leaveSent != null && votersAfterLeave == null) {
|
||||||
|
const after = await report(n0, 3);
|
||||||
|
const v = after?.checkpoints?.at(-1)?.voters ?? null;
|
||||||
|
if (v != null && votersBeforeLeave != null && v < votersBeforeLeave) { votersAfterLeave = v; leaveSeenAt = { at: +since(), daa }; log(`LEAVE TOOK EFFECT: voters ${votersBeforeLeave} -> ${v} at daa ${daa}, ${since()} s`); }
|
||||||
|
}
|
||||||
|
if (leaveSent != null && r && r.finalityActive === false) { inactiveAfterLeave.push({ at: +since(), daa, reason: r.finalityReason }); }
|
||||||
|
if (Date.now() - lastReport > 15000) {
|
||||||
|
lastReport = Date.now();
|
||||||
|
const counts = await Promise.all(nodes.map(async n => { try { const d = await n.rpc.call('getBlockDagInfo'); return `${d.blockCount}/${String(d.sink).slice(0, 8)}`; } catch { return '?'; } }));
|
||||||
|
log(`t=${since()} s daa ${daa} epoch ${epoch} class ${cls} rung ${step} (${reps}) lock ${r?.latestLockedIndex ?? '?'} active ${r?.finalityActive ?? '?'} blocks/sink per node ${counts.join(' ')}`);
|
||||||
|
samples.push({ t: +since(), daa, epoch, class: cls, step, reps, locked: r?.latestLockedIndex ?? null, active: r?.finalityActive ?? null, nodes: counts });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await sleep(3000);
|
||||||
|
|
||||||
|
// the chain: every block with its coinbase outputs by address, and the execution layer's segment rewards by miner
|
||||||
|
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return { error: e.message }; } }));
|
||||||
|
const genesis = dag[0].pruningPointHash;
|
||||||
|
async function allBlocks(n) {
|
||||||
|
const out = []; let low = genesis; const seen = new Set();
|
||||||
|
for (let round = 0; round < 500; round++) {
|
||||||
|
const r = await n.rpc.call('getBlocks', { lowHash: low, includeBlocks: true, includeTransactions: true });
|
||||||
|
const blocks = r.blocks || [];
|
||||||
|
let added = 0;
|
||||||
|
for (const b of blocks) {
|
||||||
|
const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h);
|
||||||
|
const coinbase = (b.transactions || [])[0];
|
||||||
|
const outputs = (coinbase?.outputs || []).map(o => ({ address: o.verboseData?.scriptPublicKeyAddress || '', value: BigInt(typeof o.value === 'string' ? o.value : Math.round(o.value)) }));
|
||||||
|
out.push({ hash: h, daa: +b.header.daaScore, version: +b.header.version, chain: !!b.verboseData?.isChainBlock, outputs }); added++;
|
||||||
|
}
|
||||||
|
if (!blocks.length || added === 0) break;
|
||||||
|
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
let blocks = [];
|
||||||
|
try { blocks = await allBlocks(n0); } catch (e) { log(`getBlocks: ${e.message}`); }
|
||||||
|
const chainBlocks = blocks.filter(b => b.chain && b.daa > 0).sort((a, b) => a.daa - b.daa);
|
||||||
|
const segments = [];
|
||||||
|
const tip = Number(BigInt(await n0.evm('eth_blockNumber', []).catch(() => '0x0')));
|
||||||
|
for (let i = 1; i <= tip; i++) { const s = await n0.evm('igneum_getSegment', [`0x${i.toString(16)}`]).catch(() => null); if (s) segments.push(s); }
|
||||||
|
const segByHash0 = new Map(segments.map(s => [String(s.hash).replace(/^0x/, '').toLowerCase(), s]));
|
||||||
|
ADDR = addressesFromMinerLogs();
|
||||||
|
const isPool = (o) => o.address === '' || o.address == null;
|
||||||
|
const hashKey = (h) => String(h).replace(/^0x/, '').toLowerCase();
|
||||||
|
const byHash = new Map(blocks.map(b => [hashKey(b.hash), b]));
|
||||||
|
// What each coinbase pays and what each segment credits (the node lane's reading of 7 October 2026, confirmed on the
|
||||||
|
// chain here): the coinbase of chain block b pays b's mergeset, which is chain block b-1 (its selected parent) plus the
|
||||||
|
// side blocks b merges; the execution record of chain block b credits b ITSELF plus the side blues b merges. So a chain
|
||||||
|
// block is paid by its child's coinbase and credited in its own segment, one record apart; a side blue is paid and
|
||||||
|
// credited in the same chain block. Totals per miner over the chain agree exactly once the tip's own credit (not yet
|
||||||
|
// paid by a child) is set aside.
|
||||||
|
const chainIndex = new Map(chainBlocks.map((b, i) => [hashKey(b.hash), i]));
|
||||||
|
const sidesOf = (seg, selfHash) => (seg?.mergeset || []).filter(m => hashKey(m.hash) !== selfHash);
|
||||||
|
const segHoldsItself = chainBlocks.filter(b => { const seg = segByHash0.get(hashKey(b.hash)); return seg && (seg.mergeset || []).some(m => hashKey(m.hash) === hashKey(b.hash)); }).length;
|
||||||
|
log(`segments: ${segByHash0.size}; chain blocks whose own segment lists them in its mergeset: ${segHoldsItself} of ${chainBlocks.length}`);
|
||||||
|
// the TESTNET_1 schedule at the devnet's unit inside the ramp (the run is minutes long; the first glide step is a month away)
|
||||||
|
const RAMP = 7_776_000n, START = 10n, FULL = 10_000_000_000n;
|
||||||
|
const subsidyAt = (daa) => { const sDaa = BigInt(daa); return sDaa >= RAMP ? FULL : FULL * (START * RAMP + (100n - START) * sDaa) / (100n * RAMP); };
|
||||||
|
const poolShare = (a) => (a / 100n) * 20n + (a % 100n) * 20n / 100n;
|
||||||
|
const splitOf = (a, silent) => { const producerShare = a - poolShare(a); let producer = producerShare; let pool = poolShare(a); if (silent) { const bonus = producerShare * 1000n / 10000n; producer -= bonus > producer ? producer : bonus; pool += bonus; } return [producer, pool]; };
|
||||||
|
// learn each miner's UTXO address from the chain: a chain block b whose coinbase has one producer output and whose
|
||||||
|
// mergeset is its selected parent alone (segment(b) lists no side block) pays chain block b-1, whose miner is the one
|
||||||
|
// reward of segment(b-1) when that segment has no sides either
|
||||||
|
{
|
||||||
|
const learnt = new Map();
|
||||||
|
for (let i = 1; i < chainBlocks.length; i++) {
|
||||||
|
const b = chainBlocks[i], parent = chainBlocks[i - 1];
|
||||||
|
const seg = segByHash0.get(hashKey(b.hash)), segParent = segByHash0.get(hashKey(parent.hash));
|
||||||
|
if (!seg || !segParent) continue;
|
||||||
|
if (sidesOf(seg, hashKey(b.hash)).length !== 0 || sidesOf(segParent, hashKey(parent.hash)).length !== 0) continue;
|
||||||
|
const producers = b.outputs.filter(o => !isPool(o));
|
||||||
|
const rewards = segParent.rewards || [];
|
||||||
|
if (producers.length !== 1 || rewards.length !== 1) continue;
|
||||||
|
const k = EVM.indexOf(hashKey(rewards[0].miner));
|
||||||
|
if (k >= 0 && !learnt.has(k)) learnt.set(k, producers[0].address);
|
||||||
|
}
|
||||||
|
for (const [i, a] of learnt) ADDR[i] = a;
|
||||||
|
log(`learnt addresses from the chain: ${[...learnt.entries()].map(([i, a]) => `${LABELS[i]}=${a.slice(0, 24)}`).join(' ')} (${learnt.size} of ${N})`);
|
||||||
|
}
|
||||||
|
const lockDaa = firstLock?.daa ?? Infinity;
|
||||||
|
// 1. the identity per miner over the whole chain: UTXO payments x 10^10 against execution credits, the tip's own
|
||||||
|
// credit set aside (its child's coinbase does not exist yet)
|
||||||
|
const tipBlock = chainBlocks.at(-1);
|
||||||
|
const tipSeg = tipBlock ? segByHash0.get(hashKey(tipBlock.hash)) : null;
|
||||||
|
const tipOwnCredit = (k) => { if (!tipSeg) return 0n; const blues = (tipSeg.mergeset || []).filter(m => m.blue); const self = blues.findIndex(m => hashKey(m.hash) === hashKey(tipBlock.hash)); const r = (tipSeg.rewards || [])[self]; return r && hashKey(r.miner) === EVM[k] ? BigInt(r.wei) : 0n; };
|
||||||
|
const bridgeRows = [];
|
||||||
|
for (let k = 0; k < N; k++) {
|
||||||
|
const utxo = chainBlocks.reduce((s, b) => s + b.outputs.filter(o => o.address === ADDR[k]).reduce((t, o) => t + o.value, 0n), 0n);
|
||||||
|
const exec = [...segByHash0.values()].reduce((s, seg) => s + (seg.rewards || []).filter(r => hashKey(r.miner) === EVM[k]).reduce((t, r) => t + BigInt(r.wei), 0n), 0n);
|
||||||
|
const execPaid = exec - tipOwnCredit(k);
|
||||||
|
bridgeRows.push({ miner: LABELS[k], utxo_sompi: String(utxo), exec_wei: String(exec), exec_wei_less_tip: String(execPaid), identity: utxo * WEI_PER_SOMPI === execPaid, blocks_paid: chainBlocks.reduce((n, b) => n + b.outputs.filter(o => o.address === ADDR[k]).length, 0) });
|
||||||
|
}
|
||||||
|
const bridgeIdentityVoters = bridgeRows.filter(r => r.miner !== LABELS[SILENT]).every(r => r.identity && r.blocks_paid > 0);
|
||||||
|
// 2. the bonus on the UTXO side: every coinbase output after the first lock, matched to the block it pays (chain b-1
|
||||||
|
// or a side blue of b, by miner, when that miner has exactly one blue block in the mergeset), priced at that block's
|
||||||
|
// own DAA: a silent key's block is paid the bonus split, a voting key's the plain 80 percent
|
||||||
|
const bonusRows = [];
|
||||||
|
for (let i = 1; i < chainBlocks.length; i++) {
|
||||||
|
const b = chainBlocks[i];
|
||||||
|
if (b.daa <= lockDaa) continue;
|
||||||
|
const seg = segByHash0.get(hashKey(b.hash));
|
||||||
|
if (!seg) continue;
|
||||||
|
const paid = [{ hash: hashKey(chainBlocks[i - 1].hash), blue: true }, ...sidesOf(seg, hashKey(b.hash)).map(m => ({ hash: hashKey(m.hash), blue: !!m.blue }))];
|
||||||
|
const paidBlocks = paid.map(p => ({ ...p, block: byHash.get(p.hash) })).filter(p => p.block);
|
||||||
|
for (let k = 0; k < N; k++) {
|
||||||
|
const mine = paidBlocks.filter(p => p.block.outputs !== undefined && minerOf(p.block) === k);
|
||||||
|
if (mine.length !== 1 || !mine[0].blue) continue;
|
||||||
|
const out = b.outputs.filter(o => o.address === ADDR[k]);
|
||||||
|
if (out.length !== 1) continue;
|
||||||
|
const [expectVoting] = splitOf(subsidyAt(mine[0].block.daa), false);
|
||||||
|
const [expectSilent] = splitOf(subsidyAt(mine[0].block.daa), true);
|
||||||
|
bonusRows.push({ chain_daa: b.daa, paid_block_daa: mine[0].block.daa, miner: LABELS[k], silent_key: k === SILENT, utxo_producer_sompi: String(out[0].value), expect_voting: String(expectVoting), expect_silent: String(expectSilent) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function minerOf(block) { for (let k = 0; k < N; k++) if (block.outputs.some(() => false)) return -1; return block.minerIndex ?? -1; }
|
||||||
|
// a block's miner: the vote key hash is not in getBlocks' verbose data here, so take it from the segment that credits
|
||||||
|
// the block (its own segment for a chain block, the merging chain block's segment for a side blue)
|
||||||
|
for (const b of blocks) {
|
||||||
|
const own = segByHash0.get(hashKey(b.hash));
|
||||||
|
let miner = null;
|
||||||
|
if (own) { const blues = (own.mergeset || []).filter(m => m.blue); const self = blues.findIndex(m => hashKey(m.hash) === hashKey(b.hash)); const r = (own.rewards || [])[self]; if (r) miner = hashKey(r.miner); }
|
||||||
|
if (miner == null) for (const seg of segByHash0.values()) { const blues = (seg.mergeset || []).filter(m => m.blue); const j = blues.findIndex(m => hashKey(m.hash) === hashKey(b.hash)); if (j >= 0) { const r = (seg.rewards || [])[j]; if (r) { miner = hashKey(r.miner); break; } } }
|
||||||
|
b.minerIndex = miner == null ? -1 : EVM.indexOf(miner);
|
||||||
|
}
|
||||||
|
// the bonus rows were built before minerIndex existed: build them again now that every block knows its miner
|
||||||
|
bonusRows.length = 0;
|
||||||
|
for (let i = 1; i < chainBlocks.length; i++) {
|
||||||
|
const b = chainBlocks[i];
|
||||||
|
if (b.daa <= lockDaa) continue;
|
||||||
|
const seg = segByHash0.get(hashKey(b.hash));
|
||||||
|
if (!seg) continue;
|
||||||
|
const paid = [{ hash: hashKey(chainBlocks[i - 1].hash), blue: true }, ...sidesOf(seg, hashKey(b.hash)).map(m => ({ hash: hashKey(m.hash), blue: !!m.blue }))];
|
||||||
|
const paidBlocks = paid.map(p => ({ ...p, block: byHash.get(p.hash) })).filter(p => p.block);
|
||||||
|
for (let k = 0; k < N; k++) {
|
||||||
|
const mine = paidBlocks.filter(p => p.block.minerIndex === k);
|
||||||
|
if (mine.length !== 1 || !mine[0].blue) continue;
|
||||||
|
const out = b.outputs.filter(o => o.address === ADDR[k]);
|
||||||
|
if (out.length !== 1) continue;
|
||||||
|
const [expectVoting] = splitOf(subsidyAt(mine[0].block.daa), false);
|
||||||
|
const [expectSilent] = splitOf(subsidyAt(mine[0].block.daa), true);
|
||||||
|
bonusRows.push({ chain_daa: b.daa, paid_block_daa: mine[0].block.daa, miner: LABELS[k], silent_key: k === SILENT, utxo_producer_sompi: String(out[0].value), expect_voting: String(expectVoting), expect_silent: String(expectSilent) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const silentRows = bonusRows.filter(r => r.silent_key);
|
||||||
|
const votingRows = bonusRows.filter(r => !r.silent_key);
|
||||||
|
const bonusPaidOnUtxo = silentRows.length > 0 && silentRows.every(r => r.utxo_producer_sompi === r.expect_silent);
|
||||||
|
const votersPaidPlain = votingRows.length > 0 && votingRows.every(r => r.utxo_producer_sompi === r.expect_voting);
|
||||||
|
const silentPaidLessThanVoters = bonusPaidOnUtxo && votersPaidPlain;
|
||||||
|
const bridgeIdentity = bridgeRows.every(r => r.identity);
|
||||||
|
|
||||||
|
const bridgeIdentitySilent = bridgeRows.filter(r => r.miner === LABELS[SILENT]).every(r => r.identity && r.blocks_paid > 0);
|
||||||
|
|
||||||
|
const objectBytes = blocks.filter(b => b.daa > 0).reduce((m, b) => { const v = (b.version >> 8) & 0x3f; m[v] = (m[v] || 0) + 1; return m; }, {});
|
||||||
|
const ladderBits = blocks.filter(b => b.daa > 0).reduce((m, b) => { const k = (b.version & 0x8000) ? 'up' : (b.version & 0x4000) ? 'down' : 'none'; m[k] = (m[k] || 0) + 1; return m; }, {});
|
||||||
|
const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
|
||||||
|
const counts = dag.map(d => d.blockCount ?? '?');
|
||||||
|
const accepted = LABELS.map((_, i) => minerLog(i).filter(l => /ACCEPTED block/.test(l)).length);
|
||||||
|
const rejectedNode = nodes.map(n => n.grepLog(/PoW rejected|Rejected block|rejected block/i).length);
|
||||||
|
const finalReports = await Promise.all(nodes.map(n => report(n, 3)));
|
||||||
|
// the report's reason flickers for one read at every new lock (the table frozen at the lock reads 0.00 percent signing
|
||||||
|
// for a second); a pause is consecutive inactive reads, measured in seconds of the 1-s poll, never one read
|
||||||
|
function longestInactiveStretchAfter(daa) {
|
||||||
|
const xs = inactiveAfterLeave.filter(x => x.daa > daa).map(x => x.at).sort((a, b) => a - b);
|
||||||
|
let best = 0, start = null, prev = null;
|
||||||
|
for (const t of xs) { if (prev == null || t - prev > 2.5) { start = t; } prev = t; best = Math.max(best, t - start + 1); }
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
const checks = {
|
||||||
|
start_lines_on_every_node: nodes.every(n => Object.entries(START_LINES).every(([k, re]) => k === 'class_v4' || n.grepLog(re).length > 0)),
|
||||||
|
digest_equal_on_every_node: new Set(nodes.map(n => (n.grepLog(START_LINES.digest)[0] || '').replace(/^.*?digest: /, '').slice(0, 64))).size === 1,
|
||||||
|
every_epoch_class_v4_at_rung_0: epochs.size > 0 && [...epochs.values()].every(e => e.class === 4 && (e.step === 0 || e.step == null) && (e.reps === RUNG0 || e.reps == null)),
|
||||||
|
no_ladder_bits_and_object_byte_0: blocks.length > 0 && Object.keys(ladderBits).every(k => k === 'none') && Object.keys(objectBytes).every(v => +v === 0),
|
||||||
|
zero_rejected_by_nodes: rejectedNode.every(c => c === 0),
|
||||||
|
sinks_and_counts_agree: new Set(sinks).size === 1 && new Set(counts.map(String)).size === 1,
|
||||||
|
first_lock_on_every_node: firstLock != null && finalReports.every(r => r && r.latestLockedIndex > 0),
|
||||||
|
silent_key_mined_after_the_lock: silentRows.length > 0,
|
||||||
|
bonus_paid_on_the_utxo_side: silentPaidLessThanVoters && bonusPaidOnUtxo,
|
||||||
|
voters_paid_the_plain_split_on_the_utxo_side: votersPaidPlain,
|
||||||
|
bridge_identity_for_voting_keys: bridgeIdentityVoters,
|
||||||
|
bridge_identity_for_the_silent_key: bridgeIdentitySilent,
|
||||||
|
leave_accepted: leaveOutcome === 'accepted',
|
||||||
|
leave_took_effect_within_delay_and_a_window: leaveSeenAt != null && leaveSent != null && (leaveSeenAt.daa - leaveSent.daa) <= LEAVE_DELAY + 120 + 30 + 5,
|
||||||
|
// the point of the leave item: a clean departure never holds finality for a window; the pause after the leave, if any,
|
||||||
|
// ends inside the delay plus one checkpoint, never a full window (120 DAA at fast time)
|
||||||
|
// rule v3's share swings per checkpoint while CPU voters sign late; what the leave item buys is that a clean departure
|
||||||
|
// never holds finality for a window: no inactive stretch of a window (120 DAA) after the leave took effect
|
||||||
|
no_window_pause_after_the_leave_took_effect: leaveSeenAt != null && longestInactiveStretchAfter(leaveSeenAt.daa) < 120, // seconds: one block a second at fast time, so a window of 120 DAA is 120 s
|
||||||
|
finality_active_at_the_end: finalReports.every(r => r && r.finalityActive === true),
|
||||||
|
};
|
||||||
|
let pass;
|
||||||
|
if (EXPECT === 'bonus-fails') {
|
||||||
|
// the known-failed case: the executor before the fix pays the silent key the full share, every other check holds
|
||||||
|
pass = Object.entries(checks).every(([k, v]) => k === 'bridge_identity_for_the_silent_key' ? v === false : v);
|
||||||
|
} else {
|
||||||
|
pass = Object.values(checks).every(Boolean);
|
||||||
|
}
|
||||||
|
const summary = {
|
||||||
|
pass, expect: EXPECT, checks, node: IGNEUMD, miner: CPU_MINER, override, run_secs: SECS, final_daa: lastDaa,
|
||||||
|
epochs: Object.fromEntries([...epochs.entries()]), first_lock: firstLock,
|
||||||
|
leave: { sent: leaveSent, outcome: leaveOutcome, voters_before: votersBeforeLeave, voters_after: votersAfterLeave, seen_at: leaveSeenAt, delay_daa: LEAVE_DELAY, longest_inactive_stretch_secs_after_effect: leaveSeenAt ? longestInactiveStretchAfter(leaveSeenAt.daa) : null, inactive_after: inactiveAfterLeave },
|
||||||
|
blocks: { total: blocks.length, chain: chainBlocks.length, segments: segments.length, object_bytes: objectBytes, ladder_bits: ladderBits },
|
||||||
|
bridge_rows: bridgeRows, bonus_rows: bonusRows, accepted_per_miner: accepted, rejected_by_nodes: rejectedNode, sinks, block_counts: counts, samples,
|
||||||
|
final_reports: finalReports.map(r => r && { active: r.finalityActive, reason: r.finalityReason, locked: r.latestLockedIndex, voters: r.checkpoints?.at(-1)?.voters ?? null }),
|
||||||
|
};
|
||||||
|
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
|
||||||
|
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'} (expect ${EXPECT}): epochs ${[...epochs.entries()].map(([e, v]) => `e${e}:v${v.class}:r${v.step}`).join(' ')}; first lock ${firstLock ? `index ${firstLock.index} at daa ${firstLock.daa}` : 'none'}; silent rows ${silentRows.length} (${silentRows.slice(0, 2).map(r => `daa ${r.paid_block_daa} utxo ${r.utxo_producer_sompi} silent ${r.expect_silent} voting ${r.expect_voting}`).join('; ')}), voting rows ${votingRows.length}; bridge ${bridgeRows.map(r => `${r.miner} utxo ${r.utxo_sompi} exec-tip ${r.exec_wei_less_tip} ${r.identity}`).join('; ')}; leave ${leaveOutcome} voters ${votersBeforeLeave} -> ${votersAfterLeave}; blocks ${blocks.length} chain ${chainBlocks.length}; rejected ${rejectedNode.join('/')}; sinks ${sinks.join(' ')} at ${counts.join('/')}`);
|
||||||
|
for (const [k, v] of Object.entries(checks)) if (!v) log(`${EXPECT === 'bonus-fails' && k === 'bridge_identity_for_the_silent_key' ? 'EXPECTED FAILED CHECK' : 'FAILED CHECK'} ${k}`);
|
||||||
|
log(`summary: ${TMP}/summary.json`);
|
||||||
|
await stopAll();
|
||||||
|
process.exit(pass ? 0 : 1);
|
||||||
|
} catch (e) {
|
||||||
|
log(`FAILED: ${e?.stack || e}`);
|
||||||
|
await stopAll();
|
||||||
|
process.exit(3);
|
||||||
|
}
|
||||||
228
infra/fast-time/tn-late-join.mjs
Normal file
228
infra/fast-time/tn-late-join.mjs
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// The testnet lane's copy of infra/fast-time/headers-proof-join.mjs (ca3-v4-node e5f993d4) for the late-join gate of
|
||||||
|
// ledger N9's second half, 7 October 2026: the same join, with consensus proof verification and proving v0 from genesis
|
||||||
|
// in the override and a prover beside node A (--prover), so the joiner's proofs come from A's archive.
|
||||||
|
//
|
||||||
|
// The 0.3.17 canary's own path (7 October 2026): a FRESH node joining through IBD WITH A HEADERS PROOF a chain whose sink
|
||||||
|
// carries legal version-1026 signalling headers. The two-daemon test covers relay and headerless IBD; the proof path
|
||||||
|
// needs a chain past the pruning depth, so this harness runs one on a fast-time profile at the Prunality floor
|
||||||
|
// (finality 120, merge 60, k 18: pruning 2F + 4Mk + 2k + 2 = 4,598 DAA, set 4,600) and lets a fresh node join it.
|
||||||
|
//
|
||||||
|
// node A: override-60x.json re-depthed as above, the window object (window 120, floor 100000), IGNEUM_CLASS_SIGNAL=4,
|
||||||
|
// three CPU threads (about 2.3 blocks/s on this Mac) until its pruning point has left genesis and the sink is
|
||||||
|
// --margin blocks past that (about 35 minutes), then the miner stops.
|
||||||
|
// node B: fresh, the same override, --addpeer A. PASS (--expect join): B's log carries "Starting IBD with headers
|
||||||
|
// proof" and "IBD with peer ... completed successfully", B's sink equals A's within --watch seconds, and
|
||||||
|
// the sink's header on B reads version 1026. The known-failed shape of the gate is the 0.3.17 binary (no
|
||||||
|
// knob reproduces its raw comparison); the harness's own failed shape is `--expect stall`, which must FAIL.
|
||||||
|
//
|
||||||
|
// node infra/fast-time/headers-proof-join.mjs [--expect join|stall] [--margin 200] [--watch 600] [--threads 3]
|
||||||
|
// IGNEUMD, IGNEUM_MINER name the binaries (defaults: vendor/igneum-node-0316/target-0316/release).
|
||||||
|
|
||||||
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync, copyFileSync } from 'node:fs';
|
||||||
|
import { connectRpc } 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_0316_BIN || `${ROOT}vendor/igneum-node-0316/target-0316/release`;
|
||||||
|
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
||||||
|
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
||||||
|
const TMP = process.env.IGNEUM_TN_TMP || '/tmp/igneum-fast-time-tn-join';
|
||||||
|
const BASE = +(process.env.IGNEUM_TN_BASE_PORT || 30590), SUFFIX = +(process.env.IGNEUM_TN_SUFFIX || 997);
|
||||||
|
const NEVER = '18446744073709551615';
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? Number(args[i + 1]) : dflt; };
|
||||||
|
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
|
||||||
|
const EXPECT = sflag('expect') || 'join';
|
||||||
|
const WINDOW = Number(sflag('window') || 150);
|
||||||
|
// The testnet lane's late-join case (ledger N9's second half, 7 October 2026): --prover "<command>" starts a prover
|
||||||
|
// beside A's miner (its records land in A's blocks through the ordinary record flows; the command sees IGNEUM_GRPC
|
||||||
|
// and IGNEUM_EVM_RPC for node A), and --join-after <blocks> holds B's start until A has mined that many blocks past
|
||||||
|
// its ready point, so B joins after the pool's 600-chain-block window has passed and every proof it asks for must
|
||||||
|
// come from the peers' archives. PASS is the same join; the failed shape is the node before the archive, which
|
||||||
|
// stalls on "proofs this peer did not deliver in 20 s".
|
||||||
|
const PROVER = sflag('prover') || null;
|
||||||
|
const JOIN_AFTER = Number(sflag('join-after') || 0);
|
||||||
|
// --resume: node A is already up on BASE's ports with its chain (a harness instance whose miner ran out before the join), and
|
||||||
|
// RESUME_PIDS names pid files of the processes to stop before B joins (the rescue miner, the prover); nothing is wiped
|
||||||
|
const RESUME = args.includes('--resume');
|
||||||
|
const RESUME_PIDS = (sflag('resume-pids') || '').split(',').filter(Boolean);
|
||||||
|
const MARGIN = flag('margin', 200), WATCH = flag('watch', 600), THREADS = flag('threads', 3), MAX_MINE = flag('max-mine', 5400);
|
||||||
|
const OUT = sflag('out') || `${TMP}/late-join-expect-${EXPECT}.json`;
|
||||||
|
if (!['join', 'stall'].includes(EXPECT)) { console.error('usage: --expect join|stall'); 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); }
|
||||||
|
if (!RESUME) { 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; };
|
||||||
|
export function mergeOverrideText(text, fields) {
|
||||||
|
let out = text;
|
||||||
|
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
|
||||||
|
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
|
||||||
|
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
|
||||||
|
}
|
||||||
|
// the depths at the Prunality floor: the blockrate block's finality_depth and pruning_depth replaced in place
|
||||||
|
export function redepth(text) {
|
||||||
|
// --window: the sampled difficulty window in samples (rate 4). The default 150 (the minimum) spans 600 DAA, which the
|
||||||
|
// pruning point at DAA about 800 fills; 661 (the devnet's own) spans 2,644 DAA and is the known-failed shape found
|
||||||
|
// on the first run (7 October 2026, 05:45 UK): the joiner's walk through the sampled trusted blocks runs out at a
|
||||||
|
// gap before genesis and the rule "DAA window data has only N entries" ends the IBD. The devnet meets that for
|
||||||
|
// the 2,644 DAA after its pruning point first leaves genesis; the node fix makes a young chain's short window legal.
|
||||||
|
return text
|
||||||
|
.replace(/"finality_depth":\s*\d+/, '"finality_depth": 120')
|
||||||
|
.replace(/"pruning_depth":\s*\d+/, '"pruning_depth": 4600')
|
||||||
|
.replace(/"difficulty_window_size":\s*\d+/, `"difficulty_window_size": ${WINDOW}`);
|
||||||
|
}
|
||||||
|
const DAY_MS = field('pow_day_ms');
|
||||||
|
const override = `${TMP}/override.json`;
|
||||||
|
if (!RESUME) writeFileSync(override, mergeOverrideText(redepth(baseText), {
|
||||||
|
genesis_bits: 0x1f010000, skip_proof_of_work: false,
|
||||||
|
program_class_v3_activation_daa: NEVER, program_class_v4_activation_daa: '100000', program_class_v4_signal_window_daa: 120,
|
||||||
|
// the testnet lane's late-join case (ledger N9's second half): consensus proof verification from genesis under the
|
||||||
|
// testnet object's pinned ids and proving v0 from genesis, so the prover's records are carried and verified, and a
|
||||||
|
// joiner past the pool's window must get their proofs from the peers' archives
|
||||||
|
proving_v0_activation_daa: '0', proving_consensus_verify_daa: '0',
|
||||||
|
proving_shard_program_id: '0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a', proving_aggregator_id: '0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896',
|
||||||
|
}));
|
||||||
|
log(`expect ${EXPECT}: pruning depth 4600 (finality 120, merge 60, k 18), the window object set, signal byte 4, ${THREADS} threads, margin ${MARGIN}, watch ${WATCH} s`);
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
constructor(i, connect = null) {
|
||||||
|
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2; this.evmPort = BASE + i * 10 + 3;
|
||||||
|
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 attach() {
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
try { this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`); await this.rpc.call('getBlockDagInfo'); break; } catch { this.rpc = null; await sleep(500); }
|
||||||
|
}
|
||||||
|
if (!this.rpc) throw new Error(`n${this.i}: no live node answers on ${this.jsonPort}`);
|
||||||
|
log(`n${this.i} attached on json ${this.jsonPort} (resume)`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
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}`, `--evm-rpclisten=127.0.0.1:${this.evmPort}`,
|
||||||
|
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
|
||||||
|
if (this.connect) a.push(`--addpeer=127.0.0.1:${this.connect}`); else a.push('--outpeers=0');
|
||||||
|
const out = openSync(this.logFile, 'a');
|
||||||
|
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_CLASS_SIGNAL: '4' } });
|
||||||
|
started.push(this.proc);
|
||||||
|
writeFileSync(`${TMP}/n${this.i}.pid`, String(this.proc.pid));
|
||||||
|
await sleep(1500);
|
||||||
|
if (this.proc.exitCode != null) throw new Error(`n${this.i} exited ${this.proc.exitCode}: ${this.grepLog(/ERROR|Error|error|refused|invalid/).slice(-3).join(' | ')}`);
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
try { this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`); await this.rpc.call('getBlockDagInfo'); break; } catch { this.rpc = null; await sleep(500); }
|
||||||
|
}
|
||||||
|
if (!this.rpc) throw new Error(`n${this.i}: the RPC did not answer within 10 s`);
|
||||||
|
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}${this.connect ? ` addpeer ${this.connect}` : ''}`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
|
||||||
|
async dag() { return this.rpc.call('getBlockDagInfo'); }
|
||||||
|
async headerOf(hash) { const b = await this.rpc.call('getBlock', { hash, includeTransactions: false }); return b.block.header; }
|
||||||
|
}
|
||||||
|
function miner(name, grpc, threads, secs) {
|
||||||
|
const out = openSync(`${TMP}/${name}.log`, 'a');
|
||||||
|
const p = spawn(CPU_MINER, ['mine', grpc, String(threads), String(secs), name, '--engine', 'igneum-pow', '--payout-label', name, '--status-secs', '60', '--no-vote', '--stall-secs', '0'], { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_POW_DAY_MS: String(DAY_MS) } });
|
||||||
|
started.push(p);
|
||||||
|
writeFileSync(`${TMP}/${name}.pid`, String(p.pid));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
async function stopAll() {
|
||||||
|
for (const p of [...started].reverse()) { try { p.kill('SIGINT'); } catch { } }
|
||||||
|
await sleep(1500);
|
||||||
|
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
|
||||||
|
}
|
||||||
|
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
|
||||||
|
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
|
||||||
|
process.on('uncaughtException', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
|
||||||
|
|
||||||
|
const a = RESUME ? await new Node(0).attach() : await new Node(0).start();
|
||||||
|
const genesis = (await a.dag()).pruningPointHash;
|
||||||
|
const m = RESUME ? null : miner('a-miner', a.grpc, THREADS, MAX_MINE);
|
||||||
|
// the testnet lane's prover beside A's miner (its records reach A's blocks through the record flows)
|
||||||
|
let prover = null;
|
||||||
|
if (PROVER) {
|
||||||
|
const out = openSync(`${TMP}/prover.log`, 'a');
|
||||||
|
prover = spawn('/bin/sh', ['-c', PROVER], { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_GRPC: a.grpc, IGNEUM_EVM_RPC: `http://127.0.0.1:${a.evmPort}` } });
|
||||||
|
started.push(prover);
|
||||||
|
writeFileSync(`${TMP}/prover.pid`, String(prover.pid));
|
||||||
|
log(`prover up pid ${prover.pid}: ${PROVER}`);
|
||||||
|
}
|
||||||
|
const t0 = Date.now();
|
||||||
|
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
|
||||||
|
let lastReport = 0, pruningMovedAt = null, pruningPoint = null;
|
||||||
|
if (RESUME) { const d = await a.dag(); pruningMovedAt = { t: 0, daa: +d.virtualDaaScore, blocks: d.blockCount, resumed: true }; pruningPoint = d.pruningPointHash; log(`resume: A at ${d.blockCount} blocks, daa ${d.virtualDaaScore}, pruning point ${String(d.pruningPointHash).slice(0, 8)} (taken as moved)`); }
|
||||||
|
while (!RESUME && Date.now() - t0 < MAX_MINE * 1000) {
|
||||||
|
await sleep(10000);
|
||||||
|
const d = await a.dag();
|
||||||
|
if (pruningMovedAt == null && d.pruningPointHash !== genesis) { pruningMovedAt = { t: +since(), daa: +d.virtualDaaScore, blocks: d.blockCount }; pruningPoint = d.pruningPointHash; log(`A's pruning point left genesis at ${since()} s: ${String(d.pruningPointHash).slice(0, 8)} at DAA ${d.virtualDaaScore}, ${d.blockCount} blocks`); }
|
||||||
|
if (Date.now() - lastReport > 120000) { lastReport = Date.now(); log(`t=${since()} s A ${d.blockCount} blocks daa ${d.virtualDaaScore} pruning ${String(d.pruningPointHash).slice(0, 8)}${pruningMovedAt ? ' (moved)' : ''}`); }
|
||||||
|
if (pruningMovedAt && d.blockCount >= pruningMovedAt.blocks + MARGIN) break;
|
||||||
|
}
|
||||||
|
// --join-after: A's miner runs on this many blocks past the ready point (past the pool's 600-chain-block record
|
||||||
|
// window, so every proof B asks for must come from the peers' archives), then stops as before so B joins a still sink
|
||||||
|
if (JOIN_AFTER > 0) {
|
||||||
|
// past the pool's 600-chain-block record window, measured in DAA (one block a second here): the block count is the
|
||||||
|
// unpruned count and plateaus once the pruning point moves (seen on the pod, 7 October 2026, 13:4x UK)
|
||||||
|
const readyDaa = +(await a.dag()).virtualDaaScore;
|
||||||
|
const target = readyDaa + JOIN_AFTER;
|
||||||
|
log(`A ready at daa ${readyDaa}; mining on to daa ${target} (${JOIN_AFTER} past the ready point) before B joins`);
|
||||||
|
for (;;) {
|
||||||
|
const d = await a.rpc.call('getBlockDagInfo');
|
||||||
|
if (+d.virtualDaaScore >= target) break;
|
||||||
|
await sleep(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const f of RESUME_PIDS) { try { const pid = +readFileSync(f, 'utf8').trim(); if (pid > 1) { process.kill(pid, 'SIGINT'); log(`resume: stopped pid ${pid} from ${f}`); } } catch (e) { log(`resume: ${f}: ${e.message}`); } }
|
||||||
|
try { m?.kill('SIGINT'); } catch { }
|
||||||
|
try { prover?.kill('SIGINT'); } catch { }
|
||||||
|
await sleep(3000);
|
||||||
|
const aAtJoin = await a.dag();
|
||||||
|
const aSinkHeader = await a.headerOf(aAtJoin.sink);
|
||||||
|
log(`A ready for the join: ${aAtJoin.blockCount} blocks, ${aAtJoin.headerCount} headers, daa ${aAtJoin.virtualDaaScore}, pruning point ${String(aAtJoin.pruningPointHash).slice(0, 8)}, sink ${String(aAtJoin.sink).slice(0, 8)} version ${aSinkHeader.version}`);
|
||||||
|
if (!pruningMovedAt) { log(`SUMMARY FAIL (expect ${EXPECT}): A's pruning point never left genesis in ${MAX_MINE} s; the chain is too short for a headers-proof join`); await stopAll(); process.exit(1); }
|
||||||
|
|
||||||
|
// the fresh node (on a resume, B's directory from an earlier join is wiped so B joins fresh)
|
||||||
|
if (RESUME) rmSync(`${TMP}/n1`, { recursive: true, force: true });
|
||||||
|
const b = await new Node(1, a.p2pPort).start();
|
||||||
|
const tj = Date.now();
|
||||||
|
let joined = null, proofLine = null, doneLine = null, lastJ = 0;
|
||||||
|
while (Date.now() - tj < WATCH * 1000) {
|
||||||
|
await sleep(5000);
|
||||||
|
const d = await b.dag().catch(() => null);
|
||||||
|
proofLine = proofLine || b.grepLog(/Starting IBD with headers proof/)[0] || null;
|
||||||
|
doneLine = doneLine || b.grepLog(/IBD with peer .* completed successfully/)[0] || null;
|
||||||
|
if (d && String(d.sink) === String(aAtJoin.sink) && joined == null) { joined = { t: (Date.now() - tj) / 1000, blocks: d.blockCount, headers: d.headerCount }; log(`B reached A's sink at ${joined.t} s after the join: ${d.blockCount} blocks, ${d.headerCount} headers`); }
|
||||||
|
if (Date.now() - lastJ > 30000) { lastJ = Date.now(); log(`t+${((Date.now() - tj) / 1000).toFixed(0)} s B ${d?.blockCount} blocks ${d?.headerCount} headers sink ${d ? String(d.sink).slice(0, 8) : '?'}; proof line ${proofLine ? 'yes' : 'no'}, done ${doneLine ? 'yes' : 'no'}`); }
|
||||||
|
if (joined && doneLine) break;
|
||||||
|
}
|
||||||
|
const bSinkVersion = joined ? (await b.headerOf(aAtJoin.sink).catch(() => null))?.version ?? null : null;
|
||||||
|
const errors = b.grepLog(/flow error|header version mismatch|wrong block version|completed with error/).slice(0, 5).map(l => l.replace(/^.*?\] /, '').slice(0, 200));
|
||||||
|
const checks = {
|
||||||
|
pruning_point_moved: !!pruningMovedAt,
|
||||||
|
a_sink_signalling: aSinkHeader.version === 1026,
|
||||||
|
headers_proof_path_taken: !!proofLine,
|
||||||
|
ibd_completed: !!doneLine,
|
||||||
|
b_reached_a_sink: !!joined,
|
||||||
|
b_sink_version_1026: bSinkVersion === 1026,
|
||||||
|
no_version_refusal: !errors.some(e => /header version mismatch|wrong block version/.test(e)),
|
||||||
|
};
|
||||||
|
const good = EXPECT === 'join'
|
||||||
|
? Object.values(checks).every(Boolean)
|
||||||
|
: !(checks.b_reached_a_sink && checks.ibd_completed);
|
||||||
|
const summary = { pass: good, expect: EXPECT, window: WINDOW, pruning_depth: 4600, margin: MARGIN, threads: THREADS, a_at_join: { blocks: aAtJoin.blockCount, headers: aAtJoin.headerCount, daa: aAtJoin.virtualDaaScore, pruning: String(aAtJoin.pruningPointHash).slice(0, 16), sink: String(aAtJoin.sink).slice(0, 16), sink_version: aSinkHeader.version }, pruning_moved_at: pruningMovedAt, joined, proof_line: proofLine?.replace(/^.*?\] /, '').slice(0, 200) ?? null, done_line: doneLine?.replace(/^.*?\] /, '').slice(0, 200) ?? null, b_sink_version: bSinkVersion, b_errors: errors, checks, node: IGNEUMD, miner: CPU_MINER };
|
||||||
|
mkdirSync(OUT.replace(/\/[^/]+$/, ''), { recursive: true });
|
||||||
|
writeFileSync(OUT, JSON.stringify(summary, null, 2));
|
||||||
|
try { copyFileSync(b.logFile, OUT.replace(/\.json$/, '-b-node.log')); } catch { }
|
||||||
|
const fails = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);
|
||||||
|
log(`SUMMARY ${good ? 'PASS' : 'FAIL'} (expect ${EXPECT}): A ${aAtJoin.blockCount} blocks at DAA ${aAtJoin.virtualDaaScore} with the pruning point moved at ${pruningMovedAt?.t} s, sink version ${aSinkHeader.version}; B ${proofLine ? 'took the headers-proof path' : 'did not take the headers-proof path'}, ${doneLine ? 'IBD completed' : 'IBD not completed'}, ${joined ? `reached A's sink at ${joined.t} s with ${joined.headers} headers` : 'did not reach the sink of A'}, sink version on B ${bSinkVersion}; B errors: ${errors.length}${fails.length ? `; FAILED CHECK ${fails.join(', ')}` : ''}`);
|
||||||
|
log(`summary: ${OUT}`);
|
||||||
|
await stopAll();
|
||||||
|
process.exit(good ? 0 : 1);
|
||||||
84
infra/fast-time/tn-prover-loop.mjs
Normal file
84
infra/fast-time/tn-prover-loop.mjs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// A prover beside a fast-time node (the testnet genesis lane, 7 October 2026): every --every seconds it asks the node for
|
||||||
|
// the shards its key is assigned, takes the newest empty one it has not proved, exports the chain, cuts the fixture,
|
||||||
|
// proves the shard on the CPU (igneum-prove-host --mode compressed, SP1_PROVER=cpu), signs the record with the key and
|
||||||
|
// submits it with the proof bytes. Its records reach the node's blocks through the ordinary record flows, which is what
|
||||||
|
// the late-join gate needs: proofs carried by blocks that leave the pool's window before a joiner asks for them.
|
||||||
|
//
|
||||||
|
// IGNEUM_GRPC=grpc://127.0.0.1:P IGNEUM_EVM_RPC=http://127.0.0.1:Q node infra/fast-time/tn-prover-loop.mjs \
|
||||||
|
// --label a-miner --chain igneum-devnet-997 [--every 20] [--window 100] [--tmp /tmp/tn-prover]
|
||||||
|
// IGNEUM_MINER, IGNEUM_PROVE_HOST, IGNEUM_PROVE_EXPORT name the binaries (the miner for key-hash and sign-record).
|
||||||
|
//
|
||||||
|
// One RESULT line per event (assigned, export, prove, submit, refused) with a UTC stamp; the loop never kills anything.
|
||||||
|
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const sflag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : dflt; };
|
||||||
|
const LABEL = sflag('label', 'a-miner');
|
||||||
|
const CHAIN = sflag('chain', 'igneum-devnet-997');
|
||||||
|
const EVERY = +sflag('every', 20);
|
||||||
|
const WINDOW = +sflag('window', 100);
|
||||||
|
const TMP = sflag('tmp', '/tmp/tn-prover');
|
||||||
|
const EVM = process.env.IGNEUM_EVM_RPC || 'http://127.0.0.1:30593';
|
||||||
|
const MINER = process.env.IGNEUM_MINER;
|
||||||
|
const HOST = process.env.IGNEUM_PROVE_HOST;
|
||||||
|
const EXPORT = process.env.IGNEUM_PROVE_EXPORT;
|
||||||
|
const PAYOUT = '0x4242424242424242424242424242424242424242';
|
||||||
|
const log = (...a) => console.log(new Date().toISOString(), ...a);
|
||||||
|
for (const b of [MINER, HOST, EXPORT]) if (!b || !existsSync(b)) { console.error(`missing binary: ${b}`); process.exit(2); }
|
||||||
|
mkdirSync(TMP, { recursive: true });
|
||||||
|
|
||||||
|
async function rpc(method, params) {
|
||||||
|
const r = await fetch(EVM, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.error) throw new Error(`${method}: ${JSON.stringify(j.error)}`);
|
||||||
|
return j.result;
|
||||||
|
}
|
||||||
|
function run(bin, argv, env = {}) {
|
||||||
|
const t = Date.now();
|
||||||
|
const r = spawnSync(bin, argv, { encoding: 'utf8', env: { ...process.env, ...env }, maxBuffer: 1 << 28 });
|
||||||
|
return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}`, secs: ((Date.now() - t) / 1000).toFixed(1) };
|
||||||
|
}
|
||||||
|
const kh = run(MINER, ['key-hash', LABEL]);
|
||||||
|
const KEY_HASH = (kh.out.trim().split('\n').pop() || '').trim();
|
||||||
|
if (!/^[0-9a-f]{64}$/.test(KEY_HASH)) { console.error(`key-hash ${LABEL}: ${kh.out}`); process.exit(2); }
|
||||||
|
log(`RESULT start label=${LABEL} key=${KEY_HASH} chain=${CHAIN} evm=${EVM} every=${EVERY}s window=${WINDOW}`);
|
||||||
|
const done = new Set();
|
||||||
|
let proved = 0, refused = 0;
|
||||||
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
const work = await rpc('igneum_getAssignedShards', [[`0x${KEY_HASH}`], WINDOW]);
|
||||||
|
const mine = (work || []).filter(w => w.assigned && w.txCount === 0 && !w.paid && !done.has(`${w.number}/${w.shard}`));
|
||||||
|
mine.sort((x, y) => parseInt(y.number, 16) - parseInt(x.number, 16));
|
||||||
|
const target = mine[0];
|
||||||
|
if (!target) { await sleep(EVERY * 1000); continue; }
|
||||||
|
const number = parseInt(target.number, 16);
|
||||||
|
done.add(`${target.number}/${target.shard}`);
|
||||||
|
log(`RESULT assigned number=${number} shard=${target.shard} listed=${work.length} mine=${mine.length}`);
|
||||||
|
const plan = await rpc('igneum_getShardPlan', [target.number]);
|
||||||
|
const seq = await rpc('igneum_exportSegments', ['0x0', target.number]);
|
||||||
|
writeFileSync(`${TMP}/seq.json`, JSON.stringify(seq));
|
||||||
|
const fixture = `${TMP}/block-${number}.json`;
|
||||||
|
const ex = run(EXPORT, [`${TMP}/seq.json`, String(number), fixture, '--source', `tn late-join network block ${number}`]);
|
||||||
|
if (ex.code !== 0) { log(`RESULT export_failed number=${number} secs=${ex.secs} tail=${ex.out.split('\n').slice(-3).join(' | ').slice(0, 300)}`); continue; }
|
||||||
|
log(`RESULT export number=${number} secs=${ex.secs}`);
|
||||||
|
const results = `${TMP}/results-${number}-${target.shard}.json`;
|
||||||
|
const pr = run(HOST, [fixture, '--mode', 'compressed', '--shard', String(target.shard), '--prover', PAYOUT, '--out', results], { SP1_PROVER: 'cpu', RUST_LOG: 'off' });
|
||||||
|
writeFileSync(`${TMP}/prove-${number}-${target.shard}.log`, pr.out);
|
||||||
|
if (pr.code !== 0) { log(`RESULT prove_failed number=${number} secs=${pr.secs} tail=${pr.out.split('\n').filter(l => /RESULT|rror/.test(l)).slice(-3).join(' | ').slice(0, 400)}`); continue; }
|
||||||
|
const res = JSON.parse(readFileSync(results, 'utf8'));
|
||||||
|
log(`RESULT prove number=${number} shard=${target.shard} secs=${pr.secs} cycles=${res.cycles} proof_bytes=${res.compressed_proof_bytes}`);
|
||||||
|
const sg = run(MINER, ['sign-record', LABEL, CHAIN, plan.hash, String(number), String(target.shard), PAYOUT, res.statement, res.proof_sha256]);
|
||||||
|
if (sg.code !== 0) { log(`RESULT sign_failed number=${number} out=${sg.out.slice(0, 300)}`); continue; }
|
||||||
|
const signed = JSON.parse(sg.out.trim().split('\n').pop());
|
||||||
|
const proofHex = '0x' + readFileSync(res.proof_file).toString('hex');
|
||||||
|
const sub = await rpc('igneum_submitProofRecord', [{ record: signed.record, proof: proofHex }]);
|
||||||
|
if (sub.accepted) { proved++; log(`RESULT submit number=${number} shard=${target.shard} accepted proved_total=${proved}`); } else { refused++; log(`RESULT refused number=${number} shard=${target.shard} reason=${sub.reason} refused_total=${refused}`); }
|
||||||
|
} catch (e) {
|
||||||
|
log(`RESULT error ${String(e.message || e).slice(0, 300)}`);
|
||||||
|
await sleep(EVERY * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
infra/seed-nodes/testnet-object.json
Normal file
155
infra/seed-nodes/testnet-object.json
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
{
|
||||||
|
"timestamp_deviation_tolerance": 132,
|
||||||
|
"past_median_time_window_size": 27,
|
||||||
|
"difficulty_window_size": 661,
|
||||||
|
"min_difficulty_window_size": 150,
|
||||||
|
"difficulty_rule": "igneum-dual",
|
||||||
|
"coinbase_payload_script_public_key_max_len": 150,
|
||||||
|
"max_coinbase_payload_len": 16384,
|
||||||
|
"max_tx_inputs": 1000,
|
||||||
|
"max_tx_outputs": 1000,
|
||||||
|
"max_signature_script_len": 250000,
|
||||||
|
"max_script_public_key_len": 10000,
|
||||||
|
"mass_per_tx_byte": 1,
|
||||||
|
"mass_per_script_pub_key_byte": 10,
|
||||||
|
"mass_per_sig_op": 1000,
|
||||||
|
"block_mass_limits": {
|
||||||
|
"storage": 500000,
|
||||||
|
"compute": 500000,
|
||||||
|
"transient": 1000000
|
||||||
|
},
|
||||||
|
"block_lane_limits": {
|
||||||
|
"lanes_per_block": 50,
|
||||||
|
"gas_per_lane": 1000000000
|
||||||
|
},
|
||||||
|
"storage_mass_parameter": 1000000000000,
|
||||||
|
"deflationary_phase_daa_score": 0,
|
||||||
|
"pre_deflationary_phase_base_subsidy": 50000000000,
|
||||||
|
"skip_proof_of_work": false,
|
||||||
|
"max_block_level": 250,
|
||||||
|
"pruning_proof_m": 1000,
|
||||||
|
"blockrate": {
|
||||||
|
"target_time_per_block": 1000,
|
||||||
|
"ghostdag_k": 18,
|
||||||
|
"past_median_time_sample_rate": 10,
|
||||||
|
"difficulty_sample_rate": 4,
|
||||||
|
"max_block_parents": 10,
|
||||||
|
"mergeset_size_limit": 180,
|
||||||
|
"merge_depth": 3600,
|
||||||
|
"finality_depth": 43200,
|
||||||
|
"pruning_depth": 108000,
|
||||||
|
"coinbase_maturity": 100
|
||||||
|
},
|
||||||
|
"pre_crescendo_target_time_per_block": 1000,
|
||||||
|
"crescendo_activation": 0,
|
||||||
|
"genesis_bits": 487587840,
|
||||||
|
"finality": {
|
||||||
|
"checkpoint_interval": 30,
|
||||||
|
"checkpoint_depth": 60,
|
||||||
|
"weight_window": 2592000,
|
||||||
|
"dust": 100,
|
||||||
|
"presence_window": 240,
|
||||||
|
"aggregators": 8,
|
||||||
|
"equivocation_ban": 2592000,
|
||||||
|
"min_daa": 2592000,
|
||||||
|
"aggregator_fallback": 15,
|
||||||
|
"certificate_fold": 6,
|
||||||
|
"leave_delay": 3600
|
||||||
|
},
|
||||||
|
"pow_epoch_blocks": 3600,
|
||||||
|
"pow_epoch_lead": 600,
|
||||||
|
"pow_day_ms": 86400000,
|
||||||
|
"difficulty_v2_activation_daa": 0,
|
||||||
|
"difficulty_v3_activation_daa": 0,
|
||||||
|
"proving_v0_activation_daa": 0,
|
||||||
|
"finality_v3_activation_daa": 0,
|
||||||
|
"finality_daa_rule_activation_daa": 0,
|
||||||
|
"fork_gate_activation_daa": 18446744073709551615,
|
||||||
|
"fork_gate_window_daa": 600,
|
||||||
|
"subsidy_per_block_activation_daa": 0,
|
||||||
|
"signing_bonus_activation_daa": 0,
|
||||||
|
"signing_bonus_bps": 1000,
|
||||||
|
"finality_leave_activation_daa": 0,
|
||||||
|
"program_class_v3_activation_daa": 0,
|
||||||
|
"program_class_v4_activation_daa": 0,
|
||||||
|
"program_class_v4_signal_window_daa": 0,
|
||||||
|
"base_unit_decimals": 18,
|
||||||
|
"pow_genesis_dataset_log2": 28,
|
||||||
|
"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": 0,
|
||||||
|
"latency_ladder_window_daa": 86400,
|
||||||
|
"latency_ladder_cache_rung": {
|
||||||
|
"mib": 512,
|
||||||
|
"admissible": false
|
||||||
|
},
|
||||||
|
"latency_ladder_cache_rung_activation_daa": 0,
|
||||||
|
"sig_scheme": 0,
|
||||||
|
"sig_scheme_activation_daa": 0,
|
||||||
|
"finality_succession_activation_daa": 0,
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"fees_v1_activation_daa": 0,
|
||||||
|
"proving_v1_activation_daa": 18446744073709551615,
|
||||||
|
"proving_v1_segment_blocks": 8,
|
||||||
|
"proving_v1_unproven_daa": 600,
|
||||||
|
"proving_v1_aggregator_share_bps": 1000,
|
||||||
|
"proving_v1_fresh_rule_daa": 18446744073709551615,
|
||||||
|
"exec_restart_number": 18446744073709551615,
|
||||||
|
"exec_restart_hash": "",
|
||||||
|
"exec_restart_trust_daa": 18446744073709551615,
|
||||||
|
"exec_restart_state_root": "",
|
||||||
|
"emission": {
|
||||||
|
"launch_rate": "100000000000000000000",
|
||||||
|
"ramp_seconds": 7776000,
|
||||||
|
"ramp_start_percent": 10,
|
||||||
|
"step_seconds": 2629800,
|
||||||
|
"step_decay_q32": 4172697914,
|
||||||
|
"tail": {
|
||||||
|
"kind": "percent",
|
||||||
|
"bps_per_year": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"proving_consensus_verify_daa": 0,
|
||||||
|
"proving_shard_program_id": "0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a",
|
||||||
|
"proving_aggregator_id": "0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896"
|
||||||
|
}
|
||||||
165
tools/fleet/base-unit-gate.sh
Executable file
165
tools/fleet/base-unit-gate.sh
Executable file
|
|
@ -0,0 +1,165 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# B10 of the base-unit widening (O-2.6, docs/design/base-unit.md section 8): two nodes on the testnet params (18 decimals)
|
||||||
|
# mine for N seconds on one box, then the coinbase, the gRPC and the execution layer must read one number.
|
||||||
|
#
|
||||||
|
# Runs ON igneum-build-1 (ssh build@188.40.146.49 'bash -s' < tools/fleet/base-unit-gate.sh [seconds]), against the
|
||||||
|
# binaries of the decimals worktree's fork (target/release of /srv/builds/igneum-wt-decimals/vendor/igneum-node-decimals).
|
||||||
|
# Ports 28110 to 28191 (nothing else on the box uses them); data under /srv/builds/_gate-decimals, wiped at the start; the
|
||||||
|
# processes it starts are the only ones it stops (a pid file each, never a pattern). PASS is the last line.
|
||||||
|
#
|
||||||
|
# What it checks (each a FAIL line otherwise):
|
||||||
|
# 1. both nodes answer and B reaches the same sink and block count as A (the p2p wire carries the wide amounts)
|
||||||
|
# 2. at least MIN_BLOCKS blocks were mined
|
||||||
|
# 3. igneum-miner inspect over the last 30 blocks: every coinbase's payload subsidy is above u64::MAX (18 decimals),
|
||||||
|
# the outputs split 80/20 exactly (the UTXO side), identical on both nodes
|
||||||
|
# 4. the execution layer: eth_getBalance of the miner's EVM address is equal on both nodes and equals the sum of the
|
||||||
|
# producer shares the segments paid (igneum_getSegment over every chain block), which is the identity bridge
|
||||||
|
# 5. every reward in a segment equals producer_share(block_subsidy(daa of the rewarded block itself)) under the testnet
|
||||||
|
# schedule at 18 decimals (100 IGN a second, the 90-day ramp from 10%), computed here in exact integers (each merged
|
||||||
|
# block its own DAA: subsidy_per_block_activation_daa 0 on the testnet since the re-cut of 7 October 2026)
|
||||||
|
#
|
||||||
|
# CPU note (7 October 2026, 01:5x UK): the testnet genesis bits are 2^28 expected hashes a block and the box's CPU engine
|
||||||
|
# does 0.147 MH/s on 32 threads, so a block takes 10 to 30 minutes on CPU; the ten-minute, hundreds-of-blocks form of
|
||||||
|
# this gate needs a GPU wave box (the fleet lane's); on the box alone run it for an hour with MIN_BLOCKS=3.
|
||||||
|
#
|
||||||
|
# A fresh chain is never "synced" by the mining rule (its sink is the two-day-old genesis), so both nodes run with
|
||||||
|
# --enable-unsynced-mining, as a devnet's first node does; without it every found block is Reject(IsInIBD) (seen 00:2x UK).
|
||||||
|
#
|
||||||
|
# Known-failed case: run with GATE_EXPECT_DECIMALS=8 against the same nodes and check 3 and 5 fail (the schedule at 8
|
||||||
|
# does not match an 18-decimal chain). The self-test target below does that on the recorded output.
|
||||||
|
set -euo pipefail
|
||||||
|
SECS="${1:-600}"; MIN_BLOCKS="${MIN_BLOCKS:-60}"; THREADS="${THREADS:-48}"
|
||||||
|
BIN="${BIN:-/srv/builds/igneum-wt-decimals/vendor/igneum-node-decimals/target/release}"
|
||||||
|
ROOT="${ROOT:-/srv/builds/_gate-decimals}"; A_RPC=28110; A_P2P=28111; A_EVM=28190; B_RPC=28120; B_P2P=28121; B_EVM=28191
|
||||||
|
# WORKER=<path to igneum-worker-cuda|igneum-worker-opencl>: the GPU form (a wave box; the fleet lane rents it): the first
|
||||||
|
# pack is exported from node A, the worker serves it and the miner prepares the next seeds; THREADS is then ignored
|
||||||
|
WORKER="${WORKER:-}"
|
||||||
|
EVM_ADDR="00000000000000000000000000000000000000aa"
|
||||||
|
fail=0
|
||||||
|
say() { echo "$(date -u +%H:%M:%SZ) gate: $*"; }
|
||||||
|
die() { say "FAIL: $*"; fail=1; }
|
||||||
|
stop_all() {
|
||||||
|
for p in "$ROOT"/*.pid; do [ -f "$p" ] && kill "$(cat "$p")" 2>/dev/null || true; done
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
trap stop_all EXIT
|
||||||
|
rm -rf "$ROOT"; mkdir -p "$ROOT/a" "$ROOT/b"
|
||||||
|
[ -x "$BIN/igneumd" ] && [ -x "$BIN/igneum-miner" ] || { echo "FAIL: binaries missing in $BIN"; exit 2; }
|
||||||
|
|
||||||
|
say "starting node A (testnet params, 18 decimals)"
|
||||||
|
"$BIN/igneumd" --testnet --netsuffix=1 --enable-unsynced-mining --appdir="$ROOT/a" --listen=127.0.0.1:$A_P2P --rpclisten=127.0.0.1:$A_RPC --evm-rpclisten=127.0.0.1:$A_EVM --outpeers=1 --loglevel=info > "$ROOT/a.log" 2>&1 &
|
||||||
|
echo $! > "$ROOT/a.pid"
|
||||||
|
sleep 3
|
||||||
|
grep -m1 "Base unit" "$ROOT/a.log" || true
|
||||||
|
if grep -q -E "refusing to start|panicked" "$ROOT/a.log"; then die "node A did not start: $(grep -m1 -E 'refusing|panicked' "$ROOT/a.log")"; exit 1; fi
|
||||||
|
say "starting node B, connected to A"
|
||||||
|
"$BIN/igneumd" --testnet --netsuffix=1 --enable-unsynced-mining --appdir="$ROOT/b" --listen=127.0.0.1:$B_P2P --rpclisten=127.0.0.1:$B_RPC --evm-rpclisten=127.0.0.1:$B_EVM --connect=127.0.0.1:$A_P2P --loglevel=info > "$ROOT/b.log" 2>&1 &
|
||||||
|
echo $! > "$ROOT/b.pid"
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if curl -s -m 2 -X POST -H 'content-type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' http://127.0.0.1:$A_EVM | grep -q result \
|
||||||
|
&& curl -s -m 2 -X POST -H 'content-type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' http://127.0.0.1:$B_EVM | grep -q result; then break; fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
if [ -n "$WORKER" ]; then
|
||||||
|
say "nodes up; exporting the first pack from A, then mining $SECS s on the GPU worker $WORKER, payout to 0x$EVM_ADDR"
|
||||||
|
mkdir -p "$ROOT/packs/first" "$ROOT/packs/prepare"
|
||||||
|
"$BIN/igneum-miner" export-pack grpc://127.0.0.1:$A_RPC "$ROOT/packs/first" > "$ROOT/pack.log" 2>&1 || say "export-pack exit $? (the testnet address prefix; the miner prepares the pack itself)"
|
||||||
|
"$BIN/igneum-miner" mine grpc://127.0.0.1:$A_RPC 1 "$SECS" gate --worker "$WORKER" --worker-args "--pack $ROOT/packs/first" --prepare-packs "$ROOT/packs/prepare" --network testnet --payout-label gate --evm-address "$EVM_ADDR" --dev-fee 0 --status-secs 60 > "$ROOT/miner.log" 2>&1 || say "miner exit $?"
|
||||||
|
else
|
||||||
|
say "nodes up; mining $SECS s with $THREADS CPU threads on A, payout to 0x$EVM_ADDR"
|
||||||
|
nice -n 19 "$BIN/igneum-miner" mine grpc://127.0.0.1:$A_RPC "$THREADS" "$SECS" gate --engine igneum-pow --network testnet --payout-label gate --evm-address "$EVM_ADDR" --dev-fee 0 --status-secs 60 > "$ROOT/miner.log" 2>&1 || say "miner exit $?"
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
say "miner done; last status: $(grep -E "blocks|found|accepted" "$ROOT/miner.log" | tail -1 | cut -c1-200)"
|
||||||
|
|
||||||
|
say "check 1: both nodes agree"
|
||||||
|
"$BIN/igneum-miner" watch 6 grpc://127.0.0.1:$A_RPC grpc://127.0.0.1:$B_RPC > "$ROOT/watch.log" 2>&1 || true
|
||||||
|
tail -2 "$ROOT/watch.log" | cut -c1-240
|
||||||
|
evm() { curl -s -m 5 -X POST -H 'content-type: application/json' --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$2\",\"params\":$3}" "http://127.0.0.1:$1" ; }
|
||||||
|
TIP_A=$(evm $A_EVM eth_blockNumber '[]' | jq -r .result); TIP_B=$(evm $B_EVM eth_blockNumber '[]' | jq -r .result)
|
||||||
|
say "exec tips: A $TIP_A B $TIP_B"
|
||||||
|
[ "$TIP_A" = "$TIP_B" ] || die "exec tips differ (A $TIP_A, B $TIP_B)"
|
||||||
|
BLOCKS=$((TIP_A))
|
||||||
|
[ "$BLOCKS" -ge "$MIN_BLOCKS" ] || die "only $BLOCKS chain blocks, wanted $MIN_BLOCKS"
|
||||||
|
|
||||||
|
say "check 3: the UTXO side over the last 30 blocks on both nodes"
|
||||||
|
"$BIN/igneum-miner" inspect 30 grpc://127.0.0.1:$A_RPC grpc://127.0.0.1:$B_RPC > "$ROOT/inspect.log" 2>&1 || true
|
||||||
|
grep -c "same_on_all_nodes=true" "$ROOT/inspect.log" | sed 's/^/ blocks identical on both nodes: /'
|
||||||
|
if grep -q "MISMATCH" "$ROOT/inspect.log"; then die "80/20 mismatch: $(grep -m1 MISMATCH "$ROOT/inspect.log")"; fi
|
||||||
|
if grep -q "same_on_all_nodes=false" "$ROOT/inspect.log"; then die "a block differs between the nodes"; fi
|
||||||
|
python3 - "$ROOT/inspect.log" "${GATE_EXPECT_DECIMALS:-18}" <<'PY' || die "a coinbase payload subsidy is not the 18-decimal schedule (see above)"
|
||||||
|
import re, sys
|
||||||
|
decimals = int(sys.argv[2]); UNIT = 10 ** decimals
|
||||||
|
launch_rate = 100 * UNIT; ramp_seconds = 90 * 86400; start = 10; bps = 1
|
||||||
|
def subsidy(daa):
|
||||||
|
s = daa // bps; full = launch_rate // bps
|
||||||
|
return full if s >= ramp_seconds else full * (start * ramp_seconds + (100 - start) * s) // (100 * ramp_seconds)
|
||||||
|
n = bad = 0
|
||||||
|
for line in open(sys.argv[1]):
|
||||||
|
m = re.search(r" daa=(\d+) .*subsidy_in_payload=(\d+)", line)
|
||||||
|
if not m: continue
|
||||||
|
n += 1; daa, got = int(m.group(1)), int(m.group(2))
|
||||||
|
if got != subsidy(daa):
|
||||||
|
bad += 1
|
||||||
|
if bad <= 3: print(f" daa {daa}: payload subsidy {got} != schedule {subsidy(daa)}")
|
||||||
|
print(f" payload subsidies checked {n}, wrong {bad}; one IGN = {UNIT}; day-0 block = {subsidy(0)} base units")
|
||||||
|
assert n > 0 and bad == 0
|
||||||
|
PY
|
||||||
|
tail -1 "$ROOT/inspect.log" | cut -c1-200
|
||||||
|
|
||||||
|
say "check 4 and 5: the execution layer, both nodes, against the schedule"
|
||||||
|
BAL_A=$(evm $A_EVM eth_getBalance "[\"0x$EVM_ADDR\",\"latest\"]" | jq -r .result); BAL_B=$(evm $B_EVM eth_getBalance "[\"0x$EVM_ADDR\",\"latest\"]" | jq -r .result)
|
||||||
|
say "balances: A $BAL_A B $BAL_B"
|
||||||
|
[ "$BAL_A" = "$BAL_B" ] || die "balances differ between the nodes"
|
||||||
|
: > "$ROOT/segments.jsonl"
|
||||||
|
for n in $(seq 1 "$BLOCKS"); do evm $A_EVM igneum_getSegment "[\"$(printf '0x%x' "$n")\"]" >> "$ROOT/segments.jsonl"; echo >> "$ROOT/segments.jsonl"; done
|
||||||
|
python3 - "$ROOT/segments.jsonl" "$BAL_A" "$EVM_ADDR" "${GATE_EXPECT_DECIMALS:-18}" <<'PY' || die "the execution layer does not match the schedule (see above)"
|
||||||
|
import json, sys
|
||||||
|
path, bal_hex, addr, decimals = sys.argv[1], sys.argv[2], sys.argv[3].lower(), int(sys.argv[4])
|
||||||
|
UNIT = 10 ** decimals
|
||||||
|
# the testnet schedule (EmissionSchedule::TESTNET_1 rescaled to the unit): 100 IGN a second, a 90-day ramp from 10 percent,
|
||||||
|
# the first monthly glide step is far beyond a ten-minute gate
|
||||||
|
launch_rate = 100 * UNIT; ramp_seconds = 90 * 86400; start = 10; bps = 1
|
||||||
|
def ramp(full, s):
|
||||||
|
if s >= ramp_seconds: return full
|
||||||
|
return full * (start * ramp_seconds + (100 - start) * s) // (100 * ramp_seconds)
|
||||||
|
def subsidy(daa): return ramp(launch_rate // bps, daa // bps)
|
||||||
|
def producer(a):
|
||||||
|
q, r = divmod(a, 100); pool = q * 20 + r * 20 // 100
|
||||||
|
return a - pool
|
||||||
|
total = 0; checked = 0; bad = 0
|
||||||
|
for line in open(path):
|
||||||
|
line = line.strip()
|
||||||
|
if not line: continue
|
||||||
|
r = json.loads(line).get("result")
|
||||||
|
if not r:
|
||||||
|
print(" segment query failed:", line[:120]); bad += 1; continue
|
||||||
|
# each blue block of the mergeset is credited the subsidy of ITS OWN DAA (subsidy_per_block_activation_daa 0 on
|
||||||
|
# the testnet, ledger N8, 7 October 2026); the rewards list runs in the mergeset's blue order, one entry per blue
|
||||||
|
# block. Before N8 every reward was the chain block's subsidy and 165 of 561 segments read one ramp step low
|
||||||
|
# (the pod run of 09:4x UK, 7 October 2026: the known-failed shape of this check).
|
||||||
|
def daa_of(m):
|
||||||
|
return int(m["daaScore"], 16) if isinstance(m["daaScore"], str) else int(m["daaScore"])
|
||||||
|
blues = [m for m in r["mergeset"] if m["blue"]]
|
||||||
|
if len(blues) != len(r["rewards"]):
|
||||||
|
print(f" segment {r['number']}: {len(blues)} blue blocks but {len(r['rewards'])} rewards"); bad += 1; continue
|
||||||
|
for m, w in zip(blues, r["rewards"]):
|
||||||
|
if w["miner"].lower() != m["miner"].lower():
|
||||||
|
print(f" segment {r['number']}: reward {w['miner']} is not the blue block's miner {m['miner']}"); bad += 1; continue
|
||||||
|
daa = daa_of(m)
|
||||||
|
expect = producer(subsidy(daa))
|
||||||
|
wei = int(w["wei"], 16) if isinstance(w["wei"], str) else int(w["wei"])
|
||||||
|
checked += 1
|
||||||
|
if wei != expect:
|
||||||
|
bad += 1
|
||||||
|
if bad <= 3: print(f" segment {r['number']}: reward {wei} != expected {expect} (block daa {daa})")
|
||||||
|
if w["miner"].lower().removeprefix("0x") == addr: total += wei
|
||||||
|
bal = int(bal_hex, 16)
|
||||||
|
print(f" rewards checked {checked}, wrong {bad}; sum of our rewards {total}; eth_getBalance {bal}; one IGN = {UNIT}")
|
||||||
|
print(f" balance in IGN (8 visible digits): {bal // UNIT}.{(bal % UNIT) // (UNIT // 10**8):08d}")
|
||||||
|
assert bad == 0, "a reward disagrees with the schedule"
|
||||||
|
assert total == bal, "the balance is not the sum of the rewards (the bridge is not the identity)"
|
||||||
|
assert bal > 18446744073709551615, "the balance fits a u64: not an 18-decimal chain"
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [ "$fail" = 0 ]; then say "PASS: two nodes at 18 decimals, $BLOCKS chain blocks, the coinbase, the gRPC and the execution layer read one number"; else say "FAIL (see above)"; exit 1; fi
|
||||||
Loading…
Reference in a new issue