#!/bin/bash # peer-capture.sh — generic .before-stop hook: snapshot admin peers for reinject. # In-network vantage (rpc_chains curl): public gateway rejects admin; some hosts # firewall host→bridge-IP. Client-aware peer list → /root/rpc/peer-state/.json # # Usage: peer-capture.sh # compose-path: e.g. op/reth/lisk-mainnet-op-reth-pruned-trace 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" mkdir -p "$STATE_DIR" SAFE="$(echo "$COMPOSE_PATH" | sed 's|[^a-zA-Z0-9._-]|_|g')" OUT="$STATE_DIR/${SAFE}.json" CURL_IMAGE="${PEER_CURL_IMAGE:-curlimages/curl:8.5.0}" LOG="${PEER_HOOK_LOG:-/var/log/peer-hooks.log}" log() { echo "$(date '+%F %T') peer-capture[$COMPOSE_PATH]: $*" | tee -a "$LOG" >&2; } # Documented skips case "$COMPOSE_PATH" in *nitro*|*zksync*|*zk-sync*) log "skip (nitro/zksync)" exit 0 ;; esac BASE="$(basename "$COMPOSE_PATH" | sed 's/--.*//')" # Find execution container (same heuristics as characterize-node exec_container) EL="" 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}}') if [ -z "$EL" ]; then log "no execution container for $BASE" exit 0 fi IP="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$EL" 2>/dev/null || true)" if [ -z "$IP" ]; then log "no IP for $EL" exit 0 fi 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 90 >/dev/null rpc() { local method="$1" docker exec peer-hook-probe curl -s --max-time 8 -X POST \ -H 'Content-Type: application/json' \ --data "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"params\":[],\"id\":1}" \ "http://${IP}:8545" 2>/dev/null || true } INFO="$(rpc admin_nodeInfo)" CLIENT_NAME="$(echo "$INFO" | jq -r '.result.name // empty' 2>/dev/null || true)" PEERS_JSON="$(rpc admin_peers)" PEER_COUNT="$(echo "$PEERS_JSON" | jq -r '.result | length' 2>/dev/null || echo 0)" # Detect client type (connect-peers.sh logic) NAME_LC="$(echo "$CLIENT_NAME" | tr '[:upper:]' '[:lower:]')" CLIENT_TYPE="unknown" case "$NAME_LC" in *geth*) CLIENT_TYPE="geth" ;; *reth*) CLIENT_TYPE="reth" ;; *erigon*) CLIENT_TYPE="erigon" ;; *nethermind*) CLIENT_TYPE="nethermind" ;; *besu*) CLIENT_TYPE="besu" ;; esac TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "$PEERS_JSON" | jq -c --arg path "$COMPOSE_PATH" --arg ts "$TS" \ --arg el "$EL" --arg cn "$CLIENT_NAME" --arg ct "$CLIENT_TYPE" \ --argjson pc "${PEER_COUNT:-0}" ' { node_path: $path, captured_at: $ts, container: $el, client_name: $cn, client_type: $ct, peer_count: $pc, peers: [(.result // [])[] | select(.enode != null) | { id: .enode, client: (.name // ""), layer: "execution" }] }' > "$OUT.tmp" 2>/dev/null || { log "admin_peers failed; writing empty seed" cat > "$OUT.tmp" < "${OUT%.json}.enodes" 2>/dev/null || true docker rm -f peer-hook-probe >/dev/null 2>&1 || true log "captured ${PEER_COUNT:-0} peer(s) → $OUT" exit 0