Testnet seeds: NET=testnet profile for the seed scripts, seeds-testnet.tsv (3 Hetzner VMs), dns.sh (deSEC), the public RPC allowlist and nginx site, build-job.mjs forwards --node-tests
seed1.testnet nbg1 195.201.35.33, seed2.testnet ash 5.161.232.205, seed3.testnet sin 5.223.52.210 (5 October 2026). Ports 26810/26811/28810/26890 from seed.env; provision-seed.sh BUILD_WHERE=cross takes the PC build job's Linux igneumd from infra/cross/out. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
442a1ccd0f
commit
b0652f2d8a
16 changed files with 307 additions and 25 deletions
|
|
@ -25,11 +25,22 @@ SSH_SOURCE="${SSH_SOURCE:-any}"
|
|||
|
||||
HETZNER_TOKEN_FILE="${HETZNER_TOKEN_FILE:-$HOME/.config/igneum/hetzner-token}" # one line, the API token; never printed
|
||||
|
||||
NETWORK_ARGS="${NETWORK_ARGS:---devnet}" # the live devnet; a suffixed test network: "--devnet --devnet-suffix=20"
|
||||
# NET picks the network profile (5 October 2026): devnet = the shared devnet on the 266xx ports, seeds.tsv, firewall
|
||||
# igneum-seed; testnet = igneum-testnet-1 (--testnet --netsuffix=1) on the 268xx ports (docs/testnet/README.md section 1),
|
||||
# seeds-testnet.tsv, firewall igneum-testnet-seed, DNS names seedN.testnet.igneum.network (dns.sh).
|
||||
NET="${NET:-devnet}"
|
||||
case "$NET" in
|
||||
devnet)
|
||||
NETWORK_ARGS="${NETWORK_ARGS:---devnet}" # the live devnet; a suffixed test network: "--devnet --devnet-suffix=20"
|
||||
P2P_PORT=26611; RPC_PORT=26610; RPC_JSON_PORT=28610; EVM_RPC_PORT=26790
|
||||
FIREWALL_NAME="${FIREWALL_NAME:-igneum-seed}"; SEEDS_FILE_BASE=seeds; DNS_ZONE_SUB="" ;;
|
||||
testnet)
|
||||
NETWORK_ARGS="${NETWORK_ARGS:---testnet --netsuffix=1}"
|
||||
P2P_PORT=26811; RPC_PORT=26810; RPC_JSON_PORT=28810; EVM_RPC_PORT=26890
|
||||
FIREWALL_NAME="${FIREWALL_NAME:-igneum-testnet-seed}"; SEEDS_FILE_BASE=seeds-testnet; DNS_ZONE_SUB="testnet" ;;
|
||||
*) echo "NET must be devnet or testnet" >&2; exit 1 ;;
|
||||
esac
|
||||
SEED_PEERS="${SEED_PEERS:-}" # other seeds to --addpeer, comma separated ip:port (filled from seeds.txt by provision)
|
||||
P2P_PORT=26611
|
||||
RPC_PORT=26610
|
||||
RPC_JSON_PORT=28610
|
||||
|
||||
# The node source (shared with the 20-node network): vendor/igneum-node working tree plus igneum-pow
|
||||
NODE_SRC="${NODE_SRC:-$REPO/vendor/igneum-node}"
|
||||
|
|
|
|||
|
|
@ -38,14 +38,14 @@ else
|
|||
if ! hcloud ssh-key describe "$SSH_KEY_NAME" >/dev/null 2>&1; then
|
||||
hcloud ssh-key create --name "$SSH_KEY_NAME" --public-key-from-file "$SSH_KEY_FILE.pub" >/dev/null; log "uploaded ssh key $SSH_KEY_NAME"
|
||||
fi
|
||||
if ! hcloud firewall describe igneum-seed >/dev/null 2>&1; then
|
||||
hcloud firewall create --name igneum-seed --label igneum=seed >/dev/null
|
||||
hcloud firewall add-rule igneum-seed --direction in --protocol tcp --port 22 --source-ips "$ssh_cidr" --description ssh >/dev/null
|
||||
hcloud firewall add-rule igneum-seed --direction in --protocol tcp --port "$P2P_PORT" --source-ips 0.0.0.0/0 --source-ips ::/0 --description igneum-p2p >/dev/null
|
||||
hcloud firewall add-rule igneum-seed --direction in --protocol icmp --source-ips 0.0.0.0/0 --source-ips ::/0 --description ping >/dev/null
|
||||
log "created firewall igneum-seed (in: 22 from $ssh_cidr, $P2P_PORT from anywhere, icmp)"
|
||||
if ! hcloud firewall describe "$FIREWALL_NAME" >/dev/null 2>&1; then
|
||||
hcloud firewall create --name "$FIREWALL_NAME" --label igneum=seed --label net="$NET" >/dev/null
|
||||
hcloud firewall add-rule "$FIREWALL_NAME" --direction in --protocol tcp --port 22 --source-ips "$ssh_cidr" --description ssh >/dev/null
|
||||
hcloud firewall add-rule "$FIREWALL_NAME" --direction in --protocol tcp --port "$P2P_PORT" --source-ips 0.0.0.0/0 --source-ips ::/0 --description igneum-p2p >/dev/null
|
||||
hcloud firewall add-rule "$FIREWALL_NAME" --direction in --protocol icmp --source-ips 0.0.0.0/0 --source-ips ::/0 --description ping >/dev/null
|
||||
log "created firewall $FIREWALL_NAME (in: 22 from $ssh_cidr, $P2P_PORT from anywhere, icmp)"
|
||||
fi
|
||||
log "plan: $SEED_NAME, $SEED_TYPE, $IMAGE, $SEED_LOCATION, persistent primary IPv4, firewall igneum-seed"
|
||||
log "plan: $SEED_NAME, $SEED_TYPE, $IMAGE, $SEED_LOCATION, persistent primary IPv4, firewall $FIREWALL_NAME"
|
||||
hcloud server-type describe "$SEED_TYPE" -o json | python3 -c '
|
||||
import json, sys
|
||||
j = json.load(sys.stdin); loc = sys.argv[1]
|
||||
|
|
@ -57,7 +57,7 @@ for p in j["prices"]:
|
|||
[ "${YES:-0}" = 1 ] || { printf 'create it now (billing starts) [type yes]: '; read -r a; [ "$a" = yes ] || die "not confirmed"; }
|
||||
if hcloud server describe "$SEED_NAME" >/dev/null 2>&1; then log "$SEED_NAME exists, reusing it"; else
|
||||
hcloud server create --name "$SEED_NAME" --type "$SEED_TYPE" --image "$IMAGE" --location "$SEED_LOCATION" \
|
||||
--ssh-key "$SSH_KEY_NAME" --firewall igneum-seed --label igneum=seed --label role=seed >/dev/null
|
||||
--ssh-key "$SSH_KEY_NAME" --firewall "$FIREWALL_NAME" --label igneum=seed --label role=seed --label net="$NET" >/dev/null
|
||||
log "created $SEED_NAME"
|
||||
fi
|
||||
ip=$(hcloud server ip "$SEED_NAME")
|
||||
|
|
|
|||
37
infra/seed-nodes/dns.sh
Executable file
37
infra/seed-nodes/dns.sh
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env bash
|
||||
# DNS for the seeds (5 October 2026): one A record per seed in seeds-testnet.tsv (seedN.testnet.igneum.network) and
|
||||
# rpc.testnet.igneum.network at the first seed (the public JSON-RPC behind nginx, node/install-rpc.sh), through the
|
||||
# deSEC API (igneum.network's nameservers are ns1.desec.io and ns2.desec.org; token at ~/.config/igneum/desec-token,
|
||||
# never printed). Idempotent: an existing record set is replaced. NET=testnet ./dns.sh [--check]
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
[ "$NET" = testnet ] || die "dns.sh is for NET=testnet (the devnet seed has no DNS name)"
|
||||
DESEC_TOKEN_FILE="${DESEC_TOKEN_FILE:-$HOME/.config/igneum/desec-token}"
|
||||
[ -s "$DESEC_TOKEN_FILE" ] || die "no token at $DESEC_TOKEN_FILE"
|
||||
ZONE="igneum.network"
|
||||
auth=(-H "Authorization: Token $(tr -d '[:space:]' < "$DESEC_TOKEN_FILE")" -H "Content-Type: application/json")
|
||||
put() { # put <subname> <ip>; deSEC answers 429 to more than about one write a second, so each call retries after a pause
|
||||
local sub="$1" ip="$2" code try
|
||||
for try in 1 2 3 4 5 6; do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 "${auth[@]}" -X PUT "https://desec.io/api/v1/domains/$ZONE/rrsets/$sub/A/" \
|
||||
-d "{\"subname\":\"$sub\",\"type\":\"A\",\"ttl\":3600,\"records\":[\"$ip\"]}")
|
||||
if [ "$code" = 404 ]; then
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 "${auth[@]}" -X POST "https://desec.io/api/v1/domains/$ZONE/rrsets/" \
|
||||
-d "{\"subname\":\"$sub\",\"type\":\"A\",\"ttl\":3600,\"records\":[\"$ip\"]}")
|
||||
fi
|
||||
case "$code" in 200|201) log "$sub.$ZONE A $ip ($code)"; sleep 2; return 0 ;; 429) sleep $((try * 3)) ;; *) die "$sub.$ZONE: http $code" ;; esac
|
||||
done
|
||||
die "$sub.$ZONE: rate limited six times"
|
||||
}
|
||||
if [ "${1:-}" = --check ]; then
|
||||
while IFS=$'\t' read -r name _ _ ip _; do [ -n "$name" ] || continue; printf '%s.%s -> %s (want %s)\n' "$name" "$ZONE" "$(dig +short "$name.$ZONE" @ns1.desec.io | tr '\n' ' ')" "$ip"; done < "$SEEDS_TSV"
|
||||
printf 'rpc.%s.%s -> %s\n' "$DNS_ZONE_SUB" "$ZONE" "$(dig +short "rpc.$DNS_ZONE_SUB.$ZONE" @ns1.desec.io | tr '\n' ' ')"
|
||||
exit 0
|
||||
fi
|
||||
first=""
|
||||
while IFS=$'\t' read -r name _ _ ip _; do
|
||||
[ -n "$name" ] || continue
|
||||
put "$name" "$ip"
|
||||
[ -n "$first" ] || first="$ip"
|
||||
done < "$SEEDS_TSV"
|
||||
[ -n "$first" ] && put "rpc.$DNS_ZONE_SUB" "$first"
|
||||
log "done; propagation: dig +short seed1.$DNS_ZONE_SUB.$ZONE"
|
||||
|
|
@ -10,7 +10,7 @@ check_one() {
|
|||
local port=FAIL unit=? info= dag= peers=? known=? disk=? mem=? synced=? ver=? blocks=? headers=? sink=?
|
||||
if nc -z -w 5 "$ip" "$P2P_PORT" >/dev/null 2>&1; then port=open; fi
|
||||
local raw
|
||||
raw=$(sssh "$ip" "if systemctl is-active igneumd-v4 >/dev/null 2>&1; then echo active-v4; else systemctl is-active igneumd 2>/dev/null; fi; echo '|'; python3 /opt/igneum/bin/wrpc.py call getInfo 2>/dev/null; echo '|'; python3 /opt/igneum/bin/wrpc.py call getBlockDagInfo 2>/dev/null; echo '|'; python3 /opt/igneum/bin/wrpc.py call getConnectedPeerInfo 2>/dev/null | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get(\"peerInfo\",[])))' 2>/dev/null; echo '|'; python3 /opt/igneum/bin/wrpc.py call getPeerAddresses 2>/dev/null | python3 -c 'import json,sys; j=json.load(sys.stdin); print(len(j.get(\"knownAddresses\",[])), len(j.get(\"bannedAddresses\",[])))' 2>/dev/null; echo '|'; df -h / | awk 'NR==2 { print \$5 }'; echo '|'; free -m | awk 'NR==2 { print \$3 \"/\" \$2 \"MB\" }'" 2>/dev/null) || raw=""
|
||||
raw=$(sssh "$ip" "export IGNEUM_RPC=ws://127.0.0.1:$RPC_JSON_PORT; if systemctl is-active igneumd-v4 >/dev/null 2>&1; then echo active-v4; else systemctl is-active igneumd 2>/dev/null; fi; echo '|'; python3 /opt/igneum/bin/wrpc.py call getInfo 2>/dev/null; echo '|'; python3 /opt/igneum/bin/wrpc.py call getBlockDagInfo 2>/dev/null; echo '|'; python3 /opt/igneum/bin/wrpc.py call getConnectedPeerInfo 2>/dev/null | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get(\"peerInfo\",[])))' 2>/dev/null; echo '|'; python3 /opt/igneum/bin/wrpc.py call getPeerAddresses 2>/dev/null | python3 -c 'import json,sys; j=json.load(sys.stdin); print(len(j.get(\"knownAddresses\",[])), len(j.get(\"bannedAddresses\",[])))' 2>/dev/null; echo '|'; df -h / | awk 'NR==2 { print \$5 }'; echo '|'; free -m | awk 'NR==2 { print \$3 \"/\" \$2 \"MB\" }'" 2>/dev/null) || raw=""
|
||||
unit=$(printf '%s' "$raw" | awk -F'|' 'NR==1 { gsub(/\n/, "", $1); print $1 }' | tr -d '\n')
|
||||
info=$(printf '%s' "$raw" | awk 'BEGIN{RS="|"} NR==2' | tr -d '\n')
|
||||
dag=$(printf '%s' "$raw" | awk 'BEGIN{RS="|"} NR==3' | tr -d '\n')
|
||||
|
|
|
|||
22
infra/seed-nodes/install-rpc-from-mac.sh
Executable file
22
infra/seed-nodes/install-rpc-from-mac.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
# The public RPC on one seed (the one rpc.<net>.igneum.network points at): NET=testnet ./install-rpc-from-mac.sh seed1.testnet [email]
|
||||
# Adds the igneum-<net>-rpc firewall (80, 443 from anywhere) to that server, uploads rpc/ and node/install-rpc.sh, runs it.
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
name="${1:-}"; email="${2:-}"; [ -n "$name" ] || die "which seed?"
|
||||
ip=$(seed_ip "$name"); [ -n "$ip" ] || die "$name not in $SEEDS_TSV"
|
||||
host="rpc${DNS_ZONE_SUB:+.$DNS_ZONE_SUB}.igneum.network"
|
||||
hetzner_auth
|
||||
fw="igneum-$NET-rpc"
|
||||
if ! hcloud firewall describe "$fw" >/dev/null 2>&1; then
|
||||
hcloud firewall create --name "$fw" --label igneum=rpc --label net="$NET" >/dev/null
|
||||
hcloud firewall add-rule "$fw" --direction in --protocol tcp --port 80 --source-ips 0.0.0.0/0 --source-ips ::/0 --description http-acme >/dev/null
|
||||
hcloud firewall add-rule "$fw" --direction in --protocol tcp --port 443 --source-ips 0.0.0.0/0 --source-ips ::/0 --description https-rpc >/dev/null
|
||||
log "created firewall $fw (80, 443)"
|
||||
fi
|
||||
hcloud firewall apply-to-resource "$fw" --type server --server "$name" >/dev/null 2>&1 || true
|
||||
log "firewall $fw on $name"
|
||||
sscp "$HERE/rpc/rpc-filter.py" "$SSH_USER@$ip:/opt/igneum/bin/"
|
||||
sscp "$HERE/rpc/igneum-rpc-filter.service" "$HERE/rpc/nginx-rpc.conf" "$HERE/node/install-rpc.sh" "$SSH_USER@$ip:/root/"
|
||||
sssh "$ip" "bash /root/install-rpc.sh '$host' '$email'"
|
||||
log "public RPC: https://$host (chain id check):"
|
||||
curl -s -m 15 -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' "https://$host"; echo
|
||||
|
|
@ -5,8 +5,8 @@ REPO="$(cd "$HERE/../.." && pwd)"
|
|||
export REPO
|
||||
# shellcheck source=config.sh
|
||||
. "$HERE/config.sh"
|
||||
SEEDS_TXT="$HERE/seeds.txt"
|
||||
SEEDS_TSV="$HERE/seeds.tsv"
|
||||
SEEDS_TXT="$HERE/$SEEDS_FILE_BASE.txt"
|
||||
SEEDS_TSV="$HERE/$SEEDS_FILE_BASE.tsv"
|
||||
BUILD_DIR="$HERE/build"
|
||||
|
||||
log() { printf '%s %s\n' "$(date -u +%H:%M:%S)" "$*"; }
|
||||
|
|
|
|||
26
infra/seed-nodes/node/install-rpc.sh
Executable file
26
infra/seed-nodes/node/install-rpc.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env bash
|
||||
# Runs ON the seed as root: install-rpc.sh <rpc host, e.g. rpc.testnet.igneum.network> <contact email>
|
||||
# Expects /opt/igneum/bin/rpc-filter.py, /root/igneum-rpc-filter.service, /root/nginx-rpc.conf and /etc/igneum/seed.env
|
||||
# (EVM_RPC_PORT). Installs the filter unit, the nginx site, then certbot --nginx for the TLS certificate (port 80 and
|
||||
# 443 must be open: the Mac side adds the igneum-testnet-rpc firewall first). Idempotent.
|
||||
set -euo pipefail
|
||||
host="$1"; email="${2:-}"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
command -v nginx >/dev/null 2>&1 || { apt-get update -qq; apt-get install -y -qq nginx certbot python3-certbot-nginx >/dev/null; }
|
||||
chmod +x /opt/igneum/bin/rpc-filter.py
|
||||
python3 /opt/igneum/bin/rpc-filter.py --test
|
||||
cp /root/igneum-rpc-filter.service /etc/systemd/system/igneum-rpc-filter.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable igneum-rpc-filter >/dev/null 2>&1
|
||||
systemctl restart igneum-rpc-filter
|
||||
sed "s/RPC_HOST/$host/" /root/nginx-rpc.conf > /etc/nginx/sites-available/igneum-rpc
|
||||
ln -sf /etc/nginx/sites-available/igneum-rpc /etc/nginx/sites-enabled/igneum-rpc
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
nginx -t
|
||||
systemctl enable nginx >/dev/null 2>&1; systemctl restart nginx
|
||||
if [ ! -d "/etc/letsencrypt/live/$host" ]; then
|
||||
certbot --nginx -n --agree-tos --no-eff-email ${email:+-m "$email"} ${email:---register-unsafely-without-email} -d "$host" --redirect
|
||||
fi
|
||||
systemctl is-active igneum-rpc-filter nginx
|
||||
curl -s -m 10 -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' http://127.0.0.1:8545; echo
|
||||
curl -s -m 10 -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"admin_peers","params":[]}' http://127.0.0.1:8545; echo
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
# Runs ON the seed VM as root: install-seed.sh <name> <external-ip> "<network args>" "<peer list>"
|
||||
# Runs ON the seed VM as root: install-seed.sh <name> <external-ip> "<network args>" "<peer list>" [p2p-port rpc-port json-port evm-port]
|
||||
# Expects /opt/igneum/bin/{igneumd,igneum-miner,wrpc.py,run-seed.sh} and /root/igneumd.service.
|
||||
set -euo pipefail
|
||||
name="$1"; extip="$2"; netargs="$3"; peers="${4:-}"
|
||||
name="$1"; extip="$2"; netargs="$3"; peers="${4:-}"; p2p="${5:-26611}"; rpc="${6:-26610}"; rpcjson="${7:-28610}"; evm="${8:-26790}"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
command -v chronyd >/dev/null 2>&1 || { apt-get update -qq; apt-get install -y -qq chrony python3 >/dev/null; }
|
||||
systemctl enable --now chrony >/dev/null 2>&1 || true
|
||||
|
|
@ -15,6 +15,11 @@ SEED_NAME=$name
|
|||
EXTERNAL_IP=$extip
|
||||
NETWORK_ARGS=$netargs
|
||||
SEED_PEERS=$peers
|
||||
P2P_PORT=$p2p
|
||||
RPC_PORT=$rpc
|
||||
RPC_JSON_PORT=$rpcjson
|
||||
EVM_RPC_PORT=$evm
|
||||
IGNEUM_RPC=ws://127.0.0.1:$rpcjson
|
||||
EXTRA_ARGS=
|
||||
EOF
|
||||
cp /root/igneumd.service /etc/systemd/system/igneumd.service
|
||||
|
|
|
|||
|
|
@ -7,8 +7,12 @@
|
|||
set -euo pipefail
|
||||
. /etc/igneum/seed.env
|
||||
# shellcheck disable=SC2206
|
||||
args=($NETWORK_ARGS --appdir=/var/lib/igneum --rpclisten=127.0.0.1:26610 --rpclisten-json=127.0.0.1:28610
|
||||
--listen=0.0.0.0:26611 --externalip="$EXTERNAL_IP" --nodnsseed --disable-upnp --nologfiles --yes
|
||||
# Ports come from seed.env (5 October 2026: the testnet seeds run on 26810/26811/28810/26890); an env file without
|
||||
# them is a devnet seed on the 266xx ports. The EVM JSON-RPC binds to loopback too; node/install-rpc.sh puts nginx
|
||||
# in front of it on the seed that serves rpc.<net>.igneum.network.
|
||||
args=($NETWORK_ARGS --appdir="${APPDIR:-/var/lib/igneum}" --rpclisten=127.0.0.1:"${RPC_PORT:-26610}" --rpclisten-json=127.0.0.1:"${RPC_JSON_PORT:-28610}"
|
||||
--evm-rpclisten=127.0.0.1:"${EVM_RPC_PORT:-26790}"
|
||||
--listen=0.0.0.0:"${P2P_PORT:-26611}" --externalip="$EXTERNAL_IP" --nodnsseed --disable-upnp --nologfiles --yes
|
||||
--maxinpeers=128 --outpeers=8 --loglevel=info)
|
||||
IFS=',' read -r -a peers <<< "${SEED_PEERS:-}"
|
||||
for p in "${peers[@]}"; do [ -n "$p" ] && args+=(--addpeer="$p"); done
|
||||
|
|
|
|||
|
|
@ -14,11 +14,16 @@ CD="$HERE/../cloud-devnet"
|
|||
for try in $(seq 1 20); do sssh "$ip" true >/dev/null 2>&1 && break; log "waiting for ssh ($try)"; sleep 10; done
|
||||
sssh "$ip" true || die "ssh to $ip failed"
|
||||
|
||||
if [ "$BUILD_WHERE" = bin ]; then
|
||||
[ -x "$CD/build/bin/igneumd" ] || die "no $CD/build/bin/igneumd (run ../cloud-devnet/provision.sh build)"
|
||||
log "uploading prebuilt binaries"
|
||||
if [ "$BUILD_WHERE" = bin ] || [ "$BUILD_WHERE" = cross ]; then
|
||||
# bin: ../cloud-devnet/build/bin; cross: infra/cross/out, where infra/cross/build-linux.sh and the PC build job
|
||||
# (tools/build-job.mjs, Linux target) leave igneumd and igneum-miner; BIN_DIR overrides either
|
||||
bindir="$CD/build/bin"; [ "$BUILD_WHERE" = cross ] && bindir="$REPO/infra/cross/out"; bindir="${BIN_DIR:-$bindir}"
|
||||
[ -x "$bindir/igneumd" ] || die "no $bindir/igneumd (run ../cloud-devnet/provision.sh build, infra/cross/build-linux.sh or a Linux build job)"
|
||||
file "$bindir/igneumd" | grep -q "x86-64" || die "$bindir/igneumd is not an x86-64 binary"
|
||||
log "uploading prebuilt binaries from $bindir ($(sha256sum "$bindir/igneumd" 2>/dev/null || shasum -a 256 "$bindir/igneumd" | cut -c1-16))"
|
||||
sssh "$ip" 'mkdir -p /opt/igneum/bin'
|
||||
sscp "$CD/build/bin/igneumd" "$CD/build/bin/igneum-miner" "$SSH_USER@$ip:/opt/igneum/bin/"
|
||||
sscp "$bindir/igneumd" "$bindir/igneum-miner" "$SSH_USER@$ip:/opt/igneum/bin/"
|
||||
sssh "$ip" 'chmod +x /opt/igneum/bin/igneumd /opt/igneum/bin/igneum-miner; /opt/igneum/bin/igneumd --version | head -1' || die "the uploaded igneumd does not run on $name"
|
||||
else
|
||||
NODE_SRC="$NODE_SRC" POW_SRC="$POW_SRC" SRC_MODE="$SRC_MODE" "$CD/make-source.sh"
|
||||
cp "$CD/build/src.stamp" "$BUILD_DIR/src.stamp"
|
||||
|
|
@ -34,6 +39,6 @@ peers=$(awk -F'\t' -v n="$name" -v p="$P2P_PORT" '$1 != n { print $4 ":" p }' "$
|
|||
[ -n "$SEED_PEERS" ] && peers="${peers:+$peers,}$SEED_PEERS"
|
||||
sscp "$CD/node/wrpc.py" "$HERE/node/run-seed.sh" "$SSH_USER@$ip:/opt/igneum/bin/"
|
||||
sscp "$HERE/node/install-seed.sh" "$HERE/node/igneumd.service" "$SSH_USER@$ip:/root/"
|
||||
sssh "$ip" "bash /root/install-seed.sh '$name' '$ip' '$NETWORK_ARGS' '$peers'"
|
||||
sssh "$ip" "bash /root/install-seed.sh '$name' '$ip' '$NETWORK_ARGS' '$peers' $P2P_PORT $RPC_PORT $RPC_JSON_PORT $EVM_RPC_PORT"
|
||||
log "started. Health in 30 s:"; sleep 30
|
||||
"$HERE/health.sh" "$name" || true
|
||||
|
|
|
|||
17
infra/seed-nodes/rpc/igneum-rpc-filter.service
Normal file
17
infra/seed-nodes/rpc/igneum-rpc-filter.service
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=Igneum public RPC allowlist (between nginx and the node's EVM JSON-RPC)
|
||||
After=network-online.target igneumd.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=igneum
|
||||
Group=igneum
|
||||
EnvironmentFile=/etc/igneum/seed.env
|
||||
Environment=RPC_LISTEN=127.0.0.1:8545
|
||||
ExecStart=/bin/sh -c 'exec env RPC_UPSTREAM=http://127.0.0.1:${EVM_RPC_PORT} /usr/bin/python3 /opt/igneum/bin/rpc-filter.py'
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
MemoryMax=512M
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
29
infra/seed-nodes/rpc/nginx-rpc.conf
Normal file
29
infra/seed-nodes/rpc/nginx-rpc.conf
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# The public JSON-RPC: TLS (certbot), rate limited, proxied to the allowlist filter on 127.0.0.1:8545
|
||||
# (rpc-filter.py), which forwards to the node's EVM JSON-RPC on loopback. Installed by node/install-rpc.sh as
|
||||
# /etc/nginx/sites-available/igneum-rpc; certbot adds the TLS block.
|
||||
limit_req_zone $binary_remote_addr zone=rpc:10m rate=20r/s;
|
||||
limit_conn_zone $binary_remote_addr zone=rpcconn:10m;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name RPC_HOST;
|
||||
|
||||
client_max_body_size 256k;
|
||||
client_body_timeout 10s;
|
||||
|
||||
location / {
|
||||
limit_req zone=rpc burst=40 nodelay;
|
||||
limit_conn rpcconn 20;
|
||||
limit_req_status 429;
|
||||
limit_conn_status 429;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods "POST, GET, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Content-Type" always;
|
||||
proxy_pass http://127.0.0.1:8545;
|
||||
proxy_http_version 1.1;
|
||||
proxy_read_timeout 35s;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
116
infra/seed-nodes/rpc/rpc-filter.py
Normal file
116
infra/seed-nodes/rpc/rpc-filter.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/env python3
|
||||
"""The public JSON-RPC allowlist (5 October 2026). Sits between nginx (TLS, rate limit) and the node's Ethereum
|
||||
JSON-RPC on loopback. Read-mostly: eth_* and igneum_* read methods and eth_sendRawTransaction pass; everything else
|
||||
is answered with JSON-RPC error -32601 and never reaches the node. Batches are checked element by element. Bodies over
|
||||
BODY_LIMIT bytes are refused (413). No state, no logging of bodies.
|
||||
|
||||
Environment: RPC_UPSTREAM (default http://127.0.0.1:26890), RPC_LISTEN (default 127.0.0.1:8545).
|
||||
Self-test: rpc-filter.py --test
|
||||
"""
|
||||
import json, os, sys, urllib.request, urllib.error
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
UPSTREAM = os.environ.get("RPC_UPSTREAM", "http://127.0.0.1:26890")
|
||||
LISTEN = os.environ.get("RPC_LISTEN", "127.0.0.1:8545")
|
||||
BODY_LIMIT = 256 * 1024
|
||||
ALLOWED = {
|
||||
# eth: reads
|
||||
"eth_chainId", "eth_blockNumber", "eth_getBalance", "eth_getCode", "eth_getStorageAt", "eth_getTransactionCount",
|
||||
"eth_getBlockByNumber", "eth_getBlockByHash", "eth_getBlockReceipts", "eth_getBlockTransactionCountByNumber",
|
||||
"eth_getTransactionByHash", "eth_getTransactionByBlockNumberAndIndex", "eth_getTransactionReceipt", "eth_getLogs",
|
||||
"eth_call", "eth_estimateGas", "eth_gasPrice", "eth_maxPriorityFeePerGas", "eth_feeHistory", "eth_syncing",
|
||||
"eth_protocolVersion", "eth_mining", "eth_accounts",
|
||||
# the one write
|
||||
"eth_sendRawTransaction",
|
||||
# igneum: reads (igneum_submitProofRecord and igneum_exportSegments stay out: a public endpoint never takes a record or exports)
|
||||
"igneum_estimateGas", "igneum_getBudgets", "igneum_getProofRecords", "igneum_getProvingStatus", "igneum_getSegment",
|
||||
"igneum_getShardPlan", "igneum_getAssignedShards", "igneum_getTransactionStatus",
|
||||
# identity
|
||||
"net_version", "net_listening", "net_peerCount", "web3_clientVersion",
|
||||
}
|
||||
|
||||
def err(id_, code, msg):
|
||||
return {"jsonrpc": "2.0", "id": id_, "error": {"code": code, "message": msg}}
|
||||
|
||||
def check(req):
|
||||
"""None when the request may pass, else the error reply."""
|
||||
if not isinstance(req, dict):
|
||||
return err(None, -32600, "invalid request")
|
||||
m = req.get("method")
|
||||
if not isinstance(m, str) or m not in ALLOWED:
|
||||
return err(req.get("id"), -32601, "method not available on the public RPC")
|
||||
return None
|
||||
|
||||
def filter_body(raw):
|
||||
"""(forward: bool, reply bytes or None, upstream body bytes or None)"""
|
||||
try:
|
||||
body = json.loads(raw)
|
||||
except Exception:
|
||||
return False, json.dumps(err(None, -32700, "parse error")).encode(), None
|
||||
if isinstance(body, list):
|
||||
if not body or len(body) > 50:
|
||||
return False, json.dumps(err(None, -32600, "batch of 1 to 50 requests")).encode(), None
|
||||
bad = [check(r) for r in body]
|
||||
if all(b is None for b in bad):
|
||||
return True, None, raw
|
||||
# a batch with a refused element is answered in full here, nothing reaches the node
|
||||
return False, json.dumps([b if b is not None else err(r.get("id"), -32601, "refused with the batch") for r, b in zip(body, bad)]).encode(), None
|
||||
e = check(body)
|
||||
if e is not None:
|
||||
return False, json.dumps(e).encode(), None
|
||||
return True, None, raw
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
server_version = "igneum-rpc-filter/1"
|
||||
protocol_version = "HTTP/1.1"
|
||||
def log_message(self, *a): # nginx logs the request line; nothing here
|
||||
pass
|
||||
def _send(self, code, body, ctype="application/json"):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", ctype)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
def do_GET(self):
|
||||
self._send(200, b'{"service":"igneum public rpc","methods":"eth_* reads, igneum_* reads, eth_sendRawTransaction"}')
|
||||
def do_OPTIONS(self):
|
||||
self._send(204, b"")
|
||||
def do_POST(self):
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
if n > BODY_LIMIT:
|
||||
return self._send(413, json.dumps(err(None, -32600, "body over 256 KiB")).encode())
|
||||
raw = self.rfile.read(n)
|
||||
forward, reply, up = filter_body(raw)
|
||||
if not forward:
|
||||
return self._send(200, reply)
|
||||
try:
|
||||
r = urllib.request.urlopen(urllib.request.Request(UPSTREAM, data=up, headers={"Content-Type": "application/json"}), timeout=30)
|
||||
self._send(r.status, r.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
self._send(e.code, e.read())
|
||||
except Exception:
|
||||
self._send(502, json.dumps(err(None, -32000, "node unavailable")).encode())
|
||||
|
||||
def selftest():
|
||||
ok = lambda b: filter_body(json.dumps(b).encode())[0]
|
||||
assert ok({"jsonrpc": "2.0", "id": 1, "method": "eth_chainId", "params": []})
|
||||
assert ok({"jsonrpc": "2.0", "id": 1, "method": "eth_sendRawTransaction", "params": ["0x00"]})
|
||||
assert ok({"jsonrpc": "2.0", "id": 1, "method": "igneum_getBudgets", "params": []})
|
||||
assert not ok({"jsonrpc": "2.0", "id": 1, "method": "igneum_submitProofRecord", "params": []})
|
||||
assert not ok({"jsonrpc": "2.0", "id": 1, "method": "igneum_exportSegments", "params": []})
|
||||
assert not ok({"jsonrpc": "2.0", "id": 1, "method": "admin_peers"})
|
||||
assert not ok({"jsonrpc": "2.0", "id": 1, "method": "debug_traceTransaction"})
|
||||
assert not ok({"jsonrpc": "2.0", "id": 1})
|
||||
assert not ok([])
|
||||
assert ok([{"jsonrpc": "2.0", "id": 1, "method": "eth_chainId"}, {"jsonrpc": "2.0", "id": 2, "method": "eth_blockNumber"}])
|
||||
f, reply, _ = filter_body(json.dumps([{"jsonrpc": "2.0", "id": 1, "method": "eth_chainId"}, {"jsonrpc": "2.0", "id": 2, "method": "admin_x"}]).encode())
|
||||
assert not f and len(json.loads(reply)) == 2 and json.loads(reply)[1]["error"]["code"] == -32601
|
||||
f, reply, _ = filter_body(b"not json")
|
||||
assert not f and json.loads(reply)["error"]["code"] == -32700
|
||||
print("rpc-filter: self-test ok (%d methods allowed)" % len(ALLOWED))
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--test" in sys.argv:
|
||||
selftest(); sys.exit(0)
|
||||
host, port = LISTEN.rsplit(":", 1)
|
||||
ThreadingHTTPServer((host, int(port)), H).serve_forever()
|
||||
3
infra/seed-nodes/seeds-testnet.tsv
Normal file
3
infra/seed-nodes/seeds-testnet.tsv
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
seed1.testnet hetzner nbg1 195.201.35.33 cx23 2026-10-05T15:38:38Z
|
||||
seed2.testnet hetzner ash 5.161.232.205 cpx21 2026-10-05T15:39:24Z
|
||||
seed3.testnet hetzner sin 5.223.52.210 cpx22 2026-10-05T15:40:16Z
|
||||
|
3
infra/seed-nodes/seeds-testnet.txt
Normal file
3
infra/seed-nodes/seeds-testnet.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
195.201.35.33:26811
|
||||
5.161.232.205:26811
|
||||
5.223.52.210:26811
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
// and puts them where the packaging scripts look.
|
||||
// node tools/build-job.mjs run [--node vendor/igneum-node-v4] [--target ae432dc7] [--budget-minutes 40]
|
||||
// [--stage-minutes '{"linux":20}'] [--targets linux,windows] [--no-tests]
|
||||
// [--node-tests "kaspa-consensus-core"] [--app-tests "igneum-app"] [--no-app]
|
||||
// [--title "..."] [--no-place] [--out dir] pack + publish + watch + fetch
|
||||
// node tools/build-job.mjs publish [the same flags] pack + publish, prints the id
|
||||
// node tools/build-job.mjs watch <job id> STAGE and RESULT lines as they land
|
||||
|
|
@ -31,7 +32,7 @@ const RELAY_BASE = (cfg('relay-url') || 'https://relay.igneum.network').replace(
|
|||
|
||||
const argv = process.argv.slice(2);
|
||||
const flags = {}; const pos = [];
|
||||
const BOOL = new Set(['no-tests', 'no-place', 'no-deploy', 'help']);
|
||||
const BOOL = new Set(['no-tests', 'no-place', 'no-deploy', 'no-app', 'help']);
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a.startsWith('--')) { const k = a.slice(2); const next = argv[i + 1]; if (!BOOL.has(k) && next !== undefined && !next.startsWith('--')) { flags[k] = next; i++; } else flags[k] = true; }
|
||||
|
|
@ -198,6 +199,9 @@ function placeFor(o) {
|
|||
function publish() {
|
||||
const pack = ['packaging/windows/push-build-inputs.sh'];
|
||||
if (flags.node) pack.push('--node', flags.node);
|
||||
if (flags['node-tests']) pack.push('--node-tests', String(flags['node-tests']));
|
||||
if (flags['app-tests']) pack.push('--app-tests', String(flags['app-tests']));
|
||||
if (flags['no-app']) pack.push('--no-app');
|
||||
if (flags['no-deploy']) pack.push('--no-deploy');
|
||||
console.log(`$ ${pack.join(' ')}`);
|
||||
const r = spawnSync('bash', pack, { cwd: ROOT, stdio: 'inherit' });
|
||||
|
|
|
|||
Loading…
Reference in a new issue