igneum/docs/plans/testnet-go.md

26 KiB

Igneum public testnet: the go checklist

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.

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 INTERIM until mission item 8 (the genesis forward-compatibility fields, lane genesis-forward) lands in the object.

What runs now

Piece Where State at 16:05 UTC
igneum-testnet-1 seed 1 seed1.testnet.igneum.network = 195.201.35.33, Hetzner cx23, Nuremberg (nbg1), Debian 13 up, unit igneumd active, p2p 26811 open, 2 peers, height 0, sink = genesis
seed 2 seed2.testnet.igneum.network = 5.161.232.205, Hetzner cpx21, Ashburn (ash), Debian 13 up, 2 peers, height 0
seed 3 seed3.testnet.igneum.network = 5.223.52.210, Hetzner cpx22, Singapore (sin), Debian 13 up, 2 peers, height 0
Public JSON-RPC https://rpc.testnet.igneum.network (seed 1: nginx, Let's Encrypt, 20 req/s per address with a burst of 40, 20 connections per address, bodies to 256 KiB, then rpc-filter.py on 127.0.0.1:8545, then the node's EVM RPC on 127.0.0.1:26890) live: eth_chainId = 0x116e (4462), eth_blockNumber = 0x0, net_version = 4462; admin_peers and igneum_submitProofRecord refused with -32601
DNS deSEC (ns1.desec.io, ns2.desec.org), A records for the three seeds and rpc.testnet, TTL 3600 all four resolve from the authoritative servers
Firewalls igneum-testnet-seed (22, 26811, icmp) on all three; igneum-testnet-rpc (80, 443) on seed 1 only applied
The node binary igneumd 1c19441d (fork branch testnet-infra), built on PC 1 by build job build-20261005-154330 (WSL2 Ubuntu 24.04, glibc 2.39), sha256 a9ea25f8ada29e3ee1dfc2ccced4e088a56237511be75d5d80f7bb7a72414b10 on every seed as /opt/igneum/bin/igneumd
Mining none nothing mines; no --enable-unsynced-mining, no miner process anywhere on the testnet

Each seed's start-up log, identical on the three:

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
Consensus params digest: b7d8c915f20f5af261e69e7f4aa9c3d03a9c4a462174776502b63fa1e9ec8447 (exchanged in the p2p handshake; a peer with another digest is refused)
igneumd/2.1.0
[igneum-exec] genesis 87617621714af1bf33bd17f291f90a7e0bff760a669bba53083ea8c0f7cbd840 executed: chain id 4462, registry at 0x0000000000000000000000000000000000000210

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.

The genesis, re-cut (7 October 2026; INTERIM until mission item 8 lands)

Field Value
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 (unchanged from 5 October)
Coinbase subsidy field 16 little-endian bytes, one IGN = 10^18 base units (the 18-decimal layout, O-2.6)
Timestamp 1,791,158,400,000 ms = 2026-10-05T00:00:00Z (unchanged)
Bits 0x1d100000 (unchanged)
Hash 01294fd322704dc28fbef0e7a5ef86d6ee260ac5efaf88891cdba661b5fd58ac
Merkle root bcd0e8fb1099aeb3cc1370b50e6dec9f615e204374b2097f25b2438e7d22f49f
Consensus digest 80af8aa18b9b1fd4dbe3c8640349573eb44ab518a7a90a99d8718647ebb53a9d (09:5x UK, with subsidy_per_block_activation_daa 0; it was 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)
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
DNS seeders in TESTNET_PARAMS seed1.testnet.igneum.network, seed2.testnet.igneum.network, seed3.testnet.igneum.network
--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 re-cut); repository branch testnet-genesis-2

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 digest 9390d235.... 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).

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)

Repository Branch Head What
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-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

Tests: kaspa-consensus-core and igneum-app suites on PC 2, build job build-20261005-155547, both exit 0 (cargo's own status; the relayed log keeps only the last test result line). cargo test -p igneum-common on the Mac: 27 passed (the crate is not in the PC build inputs). The packager's self-test: all checks passed on both branches. cargo check --tests of the app on the Mac: clean.

The go: what the project lead presses, in order

# 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
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
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
6 Repository public (gh repo edit igneum-network/igneum --visibility public) the project lead 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
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)
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 GATE NOT RUN YET (7 October 2026, 10:2x UK): binaries with the archive building
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
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), lane genesis-forward; the hash and digest above move once more when they land lane genesis-forward, then this lane re-cuts NOT DONE
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
9g The fast-time line of the object on a devnet-suffix network (infra/fast-time/testnet-object.mjs, 5 nodes, four voting keys and one --no-vote key, the fast-time finality profile with leave delay 60): the start-up lines, class v4 at rung 0 through ten epochs, no ladder bits, one sink, the first lock, a signed leave accepted and the voter count dropping once the frozen table refreshes, the bonus split on the UTXO side for the silent key and the plain split for the voters, the bridge identity per miner over the whole chain (the chain block paid by its child's coinbase and credited in its own record). Runs so far on the fixed binaries: the facts of the first ten-minute runs are in the lane's report; the pass line is the run under way this lane RUNNING (10: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

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

What the project lead's go needs from him, in order: (a) the word that mission item 8 is in the object (or that the cut goes without it, which means a reset later); (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

Item USD per month, net (Hetzner API, 5 October 2026)
seed 1, cx23 nbg1 6.49
seed 2, cpx21 ash (4 GB; the only 4 GB type in the US at this price) 37.49
seed 3, cpx22 sin 30.99
three primary IPv4 1.80
total 76.77 net, about 92 gross; billed hourly, 20 TB traffic included per server

A cheaper US seed is cpx11 (2 GB, 20.49), rejected because the node keeps up to four 256 MiB lottery caches once the chain has epochs; the unit's MemoryMax=3200M would not fit. The devnet seed (igneum-seed-1, 6.49) is untouched.

What was not done, and what to watch

Item Note
The explorer not built; the public RPC serves the wallet and MetaMask
site/wallet.html RPC placeholder still a placeholder; step 8
The Windows WSL verifier wrapper, the prover's table mirror docs/plans/release-0.3.6.md section 7; step 4 above
The app's Windows side testnet-app compiles on the Mac and its suite passed on PC 2 (Linux); the Windows build is the 0.4.0 cut's job
Seeds and proofs the seeds run no proof verifier (verifier: Off); they relay and hold the chain. A seed never includes proof records, which is fine for a seed
Build inputs zip build-inputs.zip on the downloads host is ONE file for every agent: a second agent's push between a job's publish and its fetch fails that job's sha256 check. Both testnet jobs fetched the right zip (verified by sha256 before each fetch); the hazard stays until the zip carries the job id in its name
glibc a PC-built Linux binary wants glibc 2.39 (Ubuntu 24.04); the seeds are Debian 13 (2.41) for that reason, and provision-seed.sh now refuses a binary the seed cannot run. The Mac's zig cross-build (glibc 2.36) stays the route for a Debian 12 host

How to redo any part

cd infra/seed-nodes
NET=testnet ./health.sh                                   # one line per seed
NET=testnet ./dns.sh --check                              # the four A records
NET=testnet BUILD_WHERE=cross ./provision-seed.sh seed2.testnet   # re-install the node from infra/cross/out (a new binary)
NET=testnet ./install-rpc-from-mac.sh seed1.testnet       # the public RPC again (idempotent; certbot keeps its certificate)

The node binary for the seeds: node tools/build-job.mjs run --node /Users/joshm/Projects/igneum/vendor/igneum-node-testnet-infra --target ae432dc7 --targets linux --no-tests then node tools/build-job.mjs fetch <id> (lands in infra/cross/out/).