From 7087fbb850b173893a7c7fb56244306d554fa71a Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 08:03:35 +0000 Subject: [PATCH] Testnet re-cut records (7 October 2026): infra/seed-nodes/testnet-object.json (igneum-testnet-1's object as override-file JSON, from print_testnet_object; the testnet itself takes no override file), docs/testnet/README.md with the re-cut genesis 01294fd3..., merkle bcd0e8fb..., digest 9390d235... and the switches on from genesis, the ledger's X3 evidence line, and the base-unit gate script from the decimals branch (tools/fleet/base-unit-gate.sh) for the gate on this tree Co-Authored-By: Claude Fable 5.1 --- docs/fud-ledger.md | 2 +- docs/testnet/README.md | 32 ++++-- infra/seed-nodes/testnet-object.json | 146 +++++++++++++++++++++++++ tools/fleet/base-unit-gate.sh | 157 +++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 9 deletions(-) create mode 100644 infra/seed-nodes/testnet-object.json create mode 100755 tools/fleet/base-unit-gate.sh diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index c69ac6244..338019e78 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -2249,7 +2249,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. -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 "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." diff --git a/docs/testnet/README.md b/docs/testnet/README.md index ff1651daf..149f87e59 100644 --- a/docs/testnet/README.md +++ b/docs/testnet/README.md @@ -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) 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 @@ -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 (`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, -`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 @@ -40,22 +46,31 @@ read methods plus `eth_sendRawTransaction`; `infra/seed-nodes/rpc/`). | Nonce, DAA score | 0, 0 | | | 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` | -| 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 | -| Merkle root | `44acfc40b1c6647011510f3c39ddb7606f979df92ad26a2914de56e44610efad` | same | +| 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 | +| 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 | `9390d235ad1543e2aa4b49013092bd45e39a917f312610e2bd1816a65c9071e7` | 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...` is void. The devnet's `c562d70e...` is unchanged | ## 3. Consensus parameters | Parameter | Testnet (adopted) | Devnet today | Why | |---|---|---|---| | 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 | -| 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 | +| 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, 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 | +| 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; `docs/design/base-unit.md` | | 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 | | | 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 | -| 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. @@ -81,5 +96,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 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 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 | diff --git a/infra/seed-nodes/testnet-object.json b/infra/seed-nodes/testnet-object.json new file mode 100644 index 000000000..3ef75fe2f --- /dev/null +++ b/infra/seed-nodes/testnet-object.json @@ -0,0 +1,146 @@ +{ + "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, + "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, + "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" +} diff --git a/tools/fleet/base-unit-gate.sh b/tools/fleet/base-unit-gate.sh new file mode 100755 index 000000000..9eeb90227 --- /dev/null +++ b/tools/fleet/base-unit-gate.sh @@ -0,0 +1,157 @@ +#!/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 chain block)) under the testnet schedule +# at 18 decimals (100 IGN a second, the 90-day ramp from 10%), computed here in exact integers +# +# 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=: 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 + chain_hash = r["hash"] + daa = next((int(m["daaScore"], 16) if isinstance(m["daaScore"], str) else int(m["daaScore"]) for m in r["mergeset"] if m["hash"] == chain_hash), None) + if daa is None: + print(f" segment {r['number']}: the chain block is not in its own mergeset list"); bad += 1; continue + expect = producer(subsidy(daa)) + blues = [m for m in r["mergeset"] if m["blue"]] + for w in r["rewards"]: + 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} (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