Files
ethereum-rpc-docker/peer-reinject.sh
rob 9c6a4ab148 Dogfood public bootstrap peers as secondary reinject seed.
Add bootstrap-peers.sh and fall back to stakesquid.eu/bootstrap when local peerset capture is empty; optional --seed-bootstrap on connect-peers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-19 08:56:52 +00:00

172 lines
5.8 KiB
Bash
Executable File

#!/bin/bash
# peer-reinject.sh — generic .after-start hook: re-seed peers from peer-capture state.
# Uses admin_addStaticPeer for geth, admin_addPeer otherwise (connect-peers.sh).
# In-network vantage (rpc_chains). Idempotent; best-effort (never fails the restart).
#
# Seed order (one path for us and customers):
# 1. Own persisted peerset (peer-state/<compose>.json from peer-capture)
# 2. Public bootstrap list https://stakesquid.eu/bootstrap/<network>.json
# (third-party enodes only — same JSON customers curl)
#
# Usage: peer-reinject.sh <compose-path>
# Env: PEER_BOOTSTRAP=0 to disable bootstrap fallback
# BOOTSTRAP_BASE_URL to override (default https://stakesquid.eu/bootstrap)
set -euo pipefail
COMPOSE_PATH="${1:-}"
if [ -z "$COMPOSE_PATH" ]; then
echo "Usage: $0 <compose-path>" >&2
exit 1
fi
BASEPATH="$(cd "$(dirname "$0")" && pwd)"
STATE_DIR="$BASEPATH/peer-state"
SAFE="$(echo "$COMPOSE_PATH" | sed 's|[^a-zA-Z0-9._-]|_|g')"
STATE="$STATE_DIR/${SAFE}.json"
CURL_IMAGE="${PEER_CURL_IMAGE:-curlimages/curl:8.5.0}"
LOG="${PEER_HOOK_LOG:-/var/log/peer-hooks.log}"
USE_BOOTSTRAP="${PEER_BOOTSTRAP:-1}"
log() { echo "$(date '+%F %T') peer-reinject[$COMPOSE_PATH]: $*" | tee -a "$LOG" >&2; }
case "$COMPOSE_PATH" in
*nitro*|*zksync*|*zk-sync*)
log "skip (nitro/zksync)"
exit 0
;;
esac
CLIENT_TYPE="unknown"
ENODES=()
SOURCE="none"
if [ -f "$STATE" ]; then
CLIENT_TYPE="$(jq -r '.client_type // "unknown"' "$STATE" 2>/dev/null || echo unknown)"
mapfile -t ENODES < <(jq -r '.peers[].id // empty' "$STATE" 2>/dev/null | grep -E '^enode://' | head -n 50 || true)
if [ "${#ENODES[@]}" -gt 0 ]; then
SOURCE="peerset"
fi
fi
# Secondary seed: public bootstrap API (same files customers use).
if [ "${#ENODES[@]}" -eq 0 ] && [ "$USE_BOOTSTRAP" != "0" ]; then
if [ -x "$BASEPATH/bootstrap-peers.sh" ]; then
mapfile -t ENODES < <("$BASEPATH/bootstrap-peers.sh" --from-compose "$COMPOSE_PATH" 2>/dev/null | grep -E '^enode://' | head -n 25 || true)
if [ "${#ENODES[@]}" -gt 0 ]; then
SOURCE="bootstrap"
log "no local peerset — using bootstrap fallback (${#ENODES[@]} enodes)"
fi
fi
fi
if [ "${#ENODES[@]}" -eq 0 ]; then
if [ ! -f "$STATE" ]; then
log "no capture state at $STATE and bootstrap empty — nothing to reinject"
else
log "no enodes in $STATE and bootstrap empty"
fi
exit 0
fi
BASE="$(basename "$COMPOSE_PATH" | sed 's/--.*//')"
EL=""
# wait briefly for container after recreate
for _ in 1 2 3 4 5 6 7 8 9 10; do
while read -r n; do
[ -z "$n" ] && continue
case "$n" in rpc-*) ;; *) continue ;; esac
case "$n" in *-node-*|*-relay-*) continue ;; esac
core="$(echo "$n" | sed -E 's/^rpc-(.+)-[0-9]+$/\1/')"
if [ "$BASE" = "$core" ] || [[ "$BASE" == "$core"-* ]]; then
EL="$n"
break
fi
done < <(docker ps --format '{{.Names}}')
[ -n "$EL" ] && break
sleep 3
done
if [ -z "$EL" ]; then
log "no execution container yet — giving up"
exit 0
fi
IP="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$EL" 2>/dev/null || true)"
# Prefer rpc_chains IP when present
RPC_IP="$(docker inspect -f '{{(index .NetworkSettings.Networks "rpc_chains").IPAddress}}' "$EL" 2>/dev/null || true)"
if [ -n "$RPC_IP" ] && [ "$RPC_IP" != "<no value>" ]; then
IP="$RPC_IP"
fi
if [ -z "$IP" ]; then
log "no IP for $EL"
exit 0
fi
# wait for RPC
for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do
docker rm -f peer-hook-probe >/dev/null 2>&1 || true
docker image inspect "$CURL_IMAGE" >/dev/null 2>&1 || docker pull -q "$CURL_IMAGE" >/dev/null
docker run -d --name peer-hook-probe --network rpc_chains "$CURL_IMAGE" sleep 180 >/dev/null
if docker exec peer-hook-probe curl -s --max-time 5 -X POST \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"net_version","params":[],"id":1}' \
"http://${IP}:8545" 2>/dev/null | grep -q result; then
break
fi
docker rm -f peer-hook-probe >/dev/null 2>&1 || true
sleep 5
done
add_peer() {
local enode="$1"
local method
if [ "$CLIENT_TYPE" = "geth" ]; then
method="admin_addStaticPeer"
else
method="admin_addPeer"
fi
local payload
payload="$(jq -nc --arg m "$method" --arg e "$enode" \
'{jsonrpc:"2.0",method:$m,params:[$e],id:1}')"
local resp
resp="$(docker exec peer-hook-probe curl -s --max-time 8 -X POST \
-H 'Content-Type: application/json' --data "$payload" \
"http://${IP}:8545" 2>/dev/null || true)"
if echo "$resp" | grep -qi 'method not found'; then
if [ "$method" = "admin_addStaticPeer" ]; then
method="admin_addPeer"
else
method="admin_addStaticPeer"
fi
payload="$(jq -nc --arg m "$method" --arg e "$enode" \
'{jsonrpc:"2.0",method:$m,params:[$e],id:1}')"
resp="$(docker exec peer-hook-probe curl -s --max-time 8 -X POST \
-H 'Content-Type: application/json' --data "$payload" \
"http://${IP}:8545" 2>/dev/null || true)"
fi
if echo "$resp" | grep -q '"result":true'; then
return 0
fi
return 1
}
ADDED=0
FAIL=0
for enode in "${ENODES[@]}"; do
if add_peer "$enode"; then
ADDED=$((ADDED + 1))
else
FAIL=$((FAIL + 1))
fi
done
# post count
COUNT="$(docker exec peer-hook-probe curl -s --max-time 8 -X POST \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"admin_peers","params":[],"id":1}' \
"http://${IP}:8545" 2>/dev/null | jq -r '.result | length' 2>/dev/null || echo '?')"
docker rm -f peer-hook-probe >/dev/null 2>&1 || true
log "reinjected source=$SOURCE added=$ADDED fail=$FAIL now_peers=$COUNT (from ${#ENODES[@]} enodes)"
exit 0