#!/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