#!/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/.json from peer-capture) # 2. Public bootstrap list https://stakesquid.eu/bootstrap/.json # (third-party enodes only — same JSON customers curl) # # Usage: peer-reinject.sh # 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 " >&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" != "" ]; 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