From 9c6a4ab14874edcbdf4cb5c3c2ec29e846a69195 Mon Sep 17 00:00:00 2001 From: rob Date: Sun, 19 Jul 2026 08:56:52 +0000 Subject: [PATCH] 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 --- bootstrap-peers.sh | 102 +++++++++++++++++++++++++++++++++++++++++++++ connect-peers.sh | 34 +++++++++++++++ peer-reinject.sh | 47 +++++++++++++++++---- 3 files changed, 176 insertions(+), 7 deletions(-) create mode 100755 bootstrap-peers.sh diff --git a/bootstrap-peers.sh b/bootstrap-peers.sh new file mode 100755 index 00000000..8dc903f2 --- /dev/null +++ b/bootstrap-peers.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# bootstrap-peers.sh — fetch curated third-party bootstrap enodes for a network. +# +# Secondary seed source for peer-reinject / restore flows. Primary source remains +# the node's own peer-capture state. Same public JSON customers curl: +# https://stakesquid.eu/bootstrap/.json +# +# Usage: +# bootstrap-peers.sh # print enodes (one per line) +# bootstrap-peers.sh --json # print full JSON +# bootstrap-peers.sh --from-compose # derive network from compose path +# bootstrap-peers.sh ... --dry-run # show URL + count, no failure on empty +# +# Env: +# BOOTSTRAP_BASE_URL default https://stakesquid.eu/bootstrap +# BOOTSTRAP_MAX max enodes to emit (default 25) +set -euo pipefail + +BASE_URL="${BOOTSTRAP_BASE_URL:-https://stakesquid.eu/bootstrap}" +MAX="${BOOTSTRAP_MAX:-25}" +DRY_RUN=false +JSON_OUT=false +NETWORK="" +COMPOSE="" + +usage() { + echo "Usage: $0 [--json] [--dry-run]" >&2 + echo " $0 --from-compose [--json] [--dry-run]" >&2 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --from-compose) + COMPOSE="${2:-}"; shift 2 || usage + ;; + --json) JSON_OUT=true; shift ;; + --dry-run) DRY_RUN=true; shift ;; + -h|--help) usage ;; + -*) + echo "Unknown option: $1" >&2 + usage + ;; + *) + NETWORK="$1"; shift + ;; + esac +done + +network_from_compose() { + local path="$1" + local base + base="$(basename "$path" | sed 's/--.*//')" + # strip client/variant suffixes (same idea as fleet-peers network_from_path) + for sep in -op-reth -op-geth -op-erigon -op-nethermind \ + -reth -geth -erigon -erigon3 -besu -nethermind \ + -bsc -nitro -l2geth -bor -external-node -gaiad \ + -juno -rskj -avalanche -maru; do + case "$base" in + *"$sep"*) base="${base%%$sep*}"; break ;; + esac + done + # common leftover: -pruned / -archive / -pebble / -path / -trace / -bonsai + base="$(echo "$base" | sed -E 's/-(pruned|archive|full|pebble|path|trace|bonsai|snap|hash).*//')" + echo "$base" +} + +if [[ -n "$COMPOSE" ]]; then + NETWORK="$(network_from_compose "$COMPOSE")" +fi +if [[ -z "$NETWORK" ]]; then + usage +fi + +URL="${BASE_URL%/}/${NETWORK}.json" +TMP="$(mktemp)" +trap 'rm -f "$TMP"' EXIT + +if ! curl -fsS --max-time 15 "$URL" -o "$TMP" 2>/dev/null; then + if [[ "$DRY_RUN" == true ]]; then + echo "bootstrap-peers: dry-run fetch failed for $URL" >&2 + exit 0 + fi + echo "bootstrap-peers: fetch failed: $URL" >&2 + exit 1 +fi + +COUNT="$(jq -r '.peers | length // 0' "$TMP" 2>/dev/null || echo 0)" +UPDATED="$(jq -r '.updated // empty' "$TMP" 2>/dev/null || true)" + +if [[ "$DRY_RUN" == true ]]; then + echo "bootstrap-peers: network=$NETWORK url=$URL peers=$COUNT updated=$UPDATED" + jq -r '.peers[:3][]? | " \(.stability.tier // "?") score=\(.stability.score // "?") n_vantage=\(.stability.n_vantage_nodes // "?") \(.enode[0:60])…"' "$TMP" 2>/dev/null || true + exit 0 +fi + +if [[ "$JSON_OUT" == true ]]; then + cat "$TMP" + exit 0 +fi + +jq -r --argjson m "$MAX" '.peers[:$m][]?.enode // empty' "$TMP" 2>/dev/null | grep -E '^enode://' || true diff --git a/connect-peers.sh b/connect-peers.sh index af654eae..dc1c4216 100755 --- a/connect-peers.sh +++ b/connect-peers.sh @@ -9,6 +9,11 @@ # target-compose-file: Optional. If provided, use this compose file for target node # If not provided, use the same compose file for both source and target # +# Secondary seed (optional): --seed-bootstrap pulls third-party enodes from the +# public bootstrap API (https://stakesquid.eu/bootstrap/.json) into the +# source node after the pairwise connect — same JSON customers use; own peerset +# remains primary via peer-reinject.sh / peer-capture. +# # Exit codes: # 0 - At least one node connected successfully (bidirectional or one-way) # 1 - Failed to connect to both nodes (see output for details) @@ -65,6 +70,7 @@ fi TIMEOUT=5 DRY_RUN=false +SEED_BOOTSTRAP=false while [[ $# -gt 0 ]]; do case "$1" in @@ -76,6 +82,10 @@ while [[ $# -gt 0 ]]; do DRY_RUN=true shift ;; + --seed-bootstrap) + SEED_BOOTSTRAP=true + shift + ;; *) echo "Unknown option: $1" usage @@ -600,6 +610,30 @@ fi echo -e "${CYAN}--- Summary ---${NC}" echo "" +# Optional secondary seed from public bootstrap list (after pairwise connect). +if [[ "$SEED_BOOTSTRAP" == true ]]; then + echo -e "${CYAN}--- Bootstrap secondary seed ---${NC}" + if [[ -x "$BASEPATH/bootstrap-peers.sh" ]]; then + if [[ "$DRY_RUN" == true ]]; then + "$BASEPATH/bootstrap-peers.sh" --from-compose "$COMPOSE_FILE" --dry-run || true + else + mapfile -t BOOT_ENODES < <("$BASEPATH/bootstrap-peers.sh" --from-compose "$COMPOSE_FILE" 2>/dev/null | grep -E '^enode://' | head -n 25 || true) + BOOT_OK=0 + for be in "${BOOT_ENODES[@]:-}"; do + [[ -z "$be" ]] && continue + RESULT=$(add_static_peer "$SOURCE_URL" "$be" "$SOURCE_CLIENT") + if check_rpc_success "$RESULT"; then + BOOT_OK=$((BOOT_OK + 1)) + fi + done + echo -e " Added ${GREEN}${BOOT_OK}${NC}/${#BOOT_ENODES[@]} bootstrap enodes to source" + fi + else + echo -e "${YELLOW}bootstrap-peers.sh not found — skip${NC}" + fi + echo "" +fi + if [[ "$DRY_RUN" == true ]]; then echo -e "${GREEN}Dry run completed - no changes made${NC}" echo "" diff --git a/peer-reinject.sh b/peer-reinject.sh index 9c62dd1e..7643bbe7 100755 --- a/peer-reinject.sh +++ b/peer-reinject.sh @@ -3,7 +3,14 @@ # 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:-}" @@ -18,6 +25,7 @@ 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; } @@ -28,15 +36,35 @@ case "$COMPOSE_PATH" in ;; esac -if [ ! -f "$STATE" ]; then - log "no capture state at $STATE — nothing to reinject" - exit 0 +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 -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[@]}" -eq 0 ]; then - log "no enodes in $STATE" + 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 @@ -64,6 +92,11 @@ if [ -z "$EL" ]; then 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 @@ -134,5 +167,5 @@ COUNT="$(docker exec peer-hook-probe curl -s --max-time 8 -X POST \ "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 added=$ADDED fail=$FAIL now_peers=$COUNT (from ${#ENODES[@]} enodes)" +log "reinjected source=$SOURCE added=$ADDED fail=$FAIL now_peers=$COUNT (from ${#ENODES[@]} enodes)" exit 0