157 lines
11 KiB
Bash
Executable file
157 lines
11 KiB
Bash
Executable file
#!/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=<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
|
|
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
|