Skip to content

Commit d5b2769

Browse files
committed
feat: add Shadow network simulator automation scripts
Add client-agnostic Shadow simulator support: - run-shadow.sh: orchestrator that generates genesis, shadow.yaml, and runs shadow - generate-shadow-yaml.sh: generates shadow.yaml from validator-config.yaml using each client's cmd.sh (supports any client via client-cmds/ pattern) - shadow-devnet/genesis/validator-config.yaml: 4-node Shadow config with virtual IPs (100.0.0.x) and apiPort for Shadow's HTTP inspection - client-cmds/leanspec-cmd.sh: leanSpec client command template - generate-genesis.sh: add --genesis-time flag for fixed timestamps (Shadow uses epoch 946684860 to avoid time-of-day dependencies)
1 parent 6baba7c commit d5b2769

5 files changed

Lines changed: 506 additions & 13 deletions

File tree

client-cmds/leanspec-cmd.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/bash
2+
3+
#-----------------------lean_spec setup----------------------
4+
# expects ghcr.io/leanethereum/leanspec-node:latest docker image
5+
6+
# Set aggregator flag based on isAggregator value
7+
aggregator_flag=""
8+
if [ "$isAggregator" == "true" ]; then
9+
aggregator_flag="--is-aggregator"
10+
fi
11+
12+
# Set checkpoint sync URL when restarting with checkpoint sync
13+
checkpoint_sync_flag=""
14+
if [ -n "${checkpoint_sync_url:-}" ]; then
15+
checkpoint_sync_flag="--checkpoint-sync-url $checkpoint_sync_url"
16+
fi
17+
18+
# Build bootnode flags from nodes.yaml (one --bootnode per ENR entry)
19+
bootnode_flags=""
20+
if [ -f "$configDir/nodes.yaml" ]; then
21+
while IFS= read -r line; do
22+
# Strip leading "- " from YAML list entries
23+
enr=$(echo "$line" | sed 's/^- //')
24+
if [ -n "$enr" ]; then
25+
bootnode_flags="$bootnode_flags --bootnode $enr"
26+
fi
27+
done < "$configDir/nodes.yaml"
28+
fi
29+
30+
node_binary="uv run python -m lean_spec \
31+
--genesis $configDir/config.yaml \
32+
--validator-keys $configDir \
33+
--node-id $item \
34+
--listen /ip4/0.0.0.0/udp/$quicPort/quic-v1 \
35+
--genesis-time-now \
36+
$bootnode_flags \
37+
$aggregator_flag \
38+
$checkpoint_sync_flag"
39+
40+
node_docker="ghcr.io/leanethereum/leanspec-node:latest \
41+
--genesis /config/config.yaml \
42+
--validator-keys /config \
43+
--node-id $item \
44+
--listen /ip4/0.0.0.0/udp/$quicPort/quic-v1 \
45+
--genesis-time-now \
46+
$bootnode_flags \
47+
$aggregator_flag \
48+
$checkpoint_sync_flag"
49+
50+
# choose either binary or docker
51+
node_setup="docker"

generate-genesis.sh

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ PK_DOCKER_IMAGE="ethpandaops/eth-beacon-genesis:pk910-leanchain"
1515
# ========================================
1616
show_usage() {
1717
cat << EOF
18-
Usage: $0 <genesis-directory> [--mode local|ansible] [--offset <seconds>] [--forceKeyGen]
18+
Usage: $0 <genesis-directory> [--mode local|ansible] [--offset <seconds>] [--genesis-time <timestamp>] [--forceKeyGen]
1919
2020
Generate genesis configuration files using PK's eth-beacon-genesis tool.
2121
Generates: config.yaml, validators.yaml, nodes.yaml, genesis.json, genesis.ssz, and .key files
@@ -30,12 +30,15 @@ Options:
3030
- local: GENESIS_TIME = now + 30 seconds (default)
3131
- ansible: GENESIS_TIME = now + 360 seconds (default)
3232
--offset <seconds> Override genesis time offset in seconds (overrides mode defaults)
33+
--genesis-time <ts> Use exact genesis timestamp (unix seconds). Overrides --mode and --offset.
34+
Useful for Shadow simulator (e.g., 946684860) or replay scenarios.
3335
--forceKeyGen Force regeneration of hash-sig validator keys
3436
3537
Examples:
3638
$0 local-devnet/genesis # Local mode (30s offset)
3739
$0 ansible-devnet/genesis --mode ansible # Ansible mode (360s offset)
3840
$0 ansible-devnet/genesis --mode ansible --offset 600 # Custom 600s offset
41+
$0 shadow-devnet/genesis --genesis-time 946684860 # Shadow simulator (fixed epoch)
3942
4043
Generated Files:
4144
- config.yaml Auto-generated with GENESIS_TIME, VALIDATOR_COUNT, shuffle, and config.activeEpoch
@@ -89,6 +92,7 @@ VALIDATOR_CONFIG_FILE="$GENESIS_DIR/validator-config.yaml"
8992
SKIP_KEY_GEN="true"
9093
DEPLOYMENT_MODE="local" # Default to local mode
9194
GENESIS_TIME_OFFSET="" # Will be set based on mode or --offset flag
95+
EXACT_GENESIS_TIME="" # If set, use this exact timestamp (ignores mode/offset)
9296
shift
9397
while [[ $# -gt 0 ]]; do
9498
case "$1" in
@@ -118,6 +122,19 @@ while [[ $# -gt 0 ]]; do
118122
exit 1
119123
fi
120124
;;
125+
--genesis-time)
126+
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
127+
if ! [[ "$2" =~ ^[0-9]+$ ]]; then
128+
echo "❌ Error: --genesis-time requires a positive integer (unix timestamp)"
129+
exit 1
130+
fi
131+
EXACT_GENESIS_TIME="$2"
132+
shift 2
133+
else
134+
echo "❌ Error: --genesis-time requires a value (unix timestamp)"
135+
exit 1
136+
fi
137+
;;
121138
*)
122139
shift
123140
;;
@@ -338,21 +355,27 @@ echo ""
338355
# ========================================
339356
echo "🔧 Step 2: Generating config.yaml..."
340357

341-
# Calculate genesis time based on deployment mode or explicit offset
358+
# Calculate genesis time based on deployment mode, explicit offset, or exact timestamp
342359
# Default offsets: Local mode: 30 seconds, Ansible mode: 360 seconds
343-
TIME_NOW="$(date +%s)"
344-
if [ -n "$GENESIS_TIME_OFFSET" ]; then
345-
# Use explicit offset if provided
346-
:
347-
elif [ "$DEPLOYMENT_MODE" == "local" ]; then
348-
GENESIS_TIME_OFFSET=30
360+
if [ -n "$EXACT_GENESIS_TIME" ]; then
361+
# Use exact genesis time (e.g., for Shadow simulator)
362+
GENESIS_TIME="$EXACT_GENESIS_TIME"
363+
echo " Using exact genesis time: $GENESIS_TIME"
349364
else
350-
GENESIS_TIME_OFFSET=360
365+
TIME_NOW="$(date +%s)"
366+
if [ -n "$GENESIS_TIME_OFFSET" ]; then
367+
# Use explicit offset if provided
368+
:
369+
elif [ "$DEPLOYMENT_MODE" == "local" ]; then
370+
GENESIS_TIME_OFFSET=30
371+
else
372+
GENESIS_TIME_OFFSET=360
373+
fi
374+
GENESIS_TIME=$((TIME_NOW + GENESIS_TIME_OFFSET))
375+
echo " Deployment mode: $DEPLOYMENT_MODE"
376+
echo " Genesis time offset: ${GENESIS_TIME_OFFSET}s"
377+
echo " Genesis time: $GENESIS_TIME"
351378
fi
352-
GENESIS_TIME=$((TIME_NOW + GENESIS_TIME_OFFSET))
353-
echo " Deployment mode: $DEPLOYMENT_MODE"
354-
echo " Genesis time offset: ${GENESIS_TIME_OFFSET}s"
355-
echo " Genesis time: $GENESIS_TIME"
356379

357380
# Sum all individual validator counts from validator-config.yaml
358381
TOTAL_VALIDATORS=$(yq eval '.validators[].count' "$VALIDATOR_CONFIG_FILE" | awk '{sum+=$1} END {print sum}')

generate-shadow-yaml.sh

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# generate-shadow-yaml.sh — Generate shadow.yaml from validator-config.yaml
5+
#
6+
# Multi-client: reuses existing client-cmds/<client>-cmd.sh to get node_binary.
7+
# Works for zeam, ream, lantern, gean, or any client with a *-cmd.sh file.
8+
#
9+
# Usage:
10+
# ./generate-shadow-yaml.sh <genesis-dir> --project-root <path> [--stop-time 360s] [--output shadow.yaml]
11+
12+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13+
14+
show_usage() {
15+
cat << EOF
16+
Usage: $0 <genesis-dir> --project-root <path> [--stop-time 360s] [--output shadow.yaml]
17+
18+
Generate a Shadow network simulator configuration (shadow.yaml) from validator-config.yaml.
19+
20+
Arguments:
21+
genesis-dir Path to genesis directory containing validator-config.yaml
22+
23+
Options:
24+
--project-root <path> Project root directory (parent of lean-quickstart). Required.
25+
--stop-time <time> Shadow simulation stop time (default: 360s)
26+
--output <path> Output shadow.yaml path (default: <project-root>/shadow.yaml)
27+
28+
This script is client-agnostic. It reads node names from validator-config.yaml,
29+
extracts the client name from the node prefix (e.g., zeam_0 → zeam), and sources
30+
the corresponding client-cmds/<client>-cmd.sh to generate per-node arguments.
31+
EOF
32+
exit 1
33+
}
34+
35+
# ========================================
36+
# Parse arguments
37+
# ========================================
38+
if [ -z "$1" ] || [ "${1:0:1}" == "-" ]; then
39+
show_usage
40+
fi
41+
42+
GENESIS_DIR="$(cd "$1" && pwd)"
43+
shift
44+
45+
PROJECT_ROOT=""
46+
STOP_TIME="360s"
47+
OUTPUT_FILE=""
48+
49+
while [[ $# -gt 0 ]]; do
50+
case "$1" in
51+
--project-root)
52+
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
53+
PROJECT_ROOT="$(cd "$2" && pwd)"
54+
shift 2
55+
else
56+
echo "❌ Error: --project-root requires a path"
57+
exit 1
58+
fi
59+
;;
60+
--stop-time)
61+
if [ -n "$2" ]; then
62+
STOP_TIME="$2"
63+
shift 2
64+
else
65+
echo "❌ Error: --stop-time requires a value"
66+
exit 1
67+
fi
68+
;;
69+
--output)
70+
if [ -n "$2" ]; then
71+
OUTPUT_FILE="$2"
72+
shift 2
73+
else
74+
echo "❌ Error: --output requires a path"
75+
exit 1
76+
fi
77+
;;
78+
*)
79+
echo "❌ Unknown option: $1"
80+
show_usage
81+
;;
82+
esac
83+
done
84+
85+
if [ -z "$PROJECT_ROOT" ]; then
86+
echo "❌ Error: --project-root is required"
87+
show_usage
88+
fi
89+
90+
if [ -z "$OUTPUT_FILE" ]; then
91+
OUTPUT_FILE="$PROJECT_ROOT/shadow.yaml"
92+
fi
93+
94+
VALIDATOR_CONFIG="$GENESIS_DIR/validator-config.yaml"
95+
if [ ! -f "$VALIDATOR_CONFIG" ]; then
96+
echo "❌ Error: validator-config.yaml not found at $VALIDATOR_CONFIG"
97+
exit 1
98+
fi
99+
100+
# ========================================
101+
# Read nodes from validator-config.yaml
102+
# ========================================
103+
node_names=($(yq eval '.validators[].name' "$VALIDATOR_CONFIG"))
104+
node_count=${#node_names[@]}
105+
106+
if [ "$node_count" -eq 0 ]; then
107+
echo "❌ Error: No validators found in $VALIDATOR_CONFIG"
108+
exit 1
109+
fi
110+
111+
echo "🔧 Generating shadow.yaml for $node_count nodes..."
112+
113+
# ========================================
114+
# Write shadow.yaml preamble
115+
# ========================================
116+
cat > "$OUTPUT_FILE" << EOF
117+
# Auto-generated Shadow network simulator configuration
118+
# Generated from: $VALIDATOR_CONFIG
119+
# Nodes: ${node_names[*]}
120+
121+
general:
122+
model_unblocked_syscall_latency: true
123+
stop_time: $STOP_TIME
124+
125+
experimental:
126+
native_preemption_enabled: true
127+
128+
network:
129+
graph:
130+
type: 1_gbit_switch
131+
132+
hosts:
133+
EOF
134+
135+
# ========================================
136+
# Generate per-node host entries
137+
# ========================================
138+
for i in "${!node_names[@]}"; do
139+
item="${node_names[$i]}"
140+
141+
# Extract client name from node prefix (zeam_0 → zeam, leanspec_0 → leanspec)
142+
IFS='_' read -r -a elements <<< "$item"
143+
client="${elements[0]}"
144+
145+
# DNS-valid hostname: underscores → hyphens (Shadow requirement)
146+
hostname="${item//_/-}"
147+
148+
# Extract IP from validator-config
149+
ip=$(yq eval ".validators[$i].enrFields.ip" "$VALIDATOR_CONFIG")
150+
151+
# Set up environment for parse-vc.sh and client-cmd.sh
152+
# These scripts expect: $item, $configDir, $dataDir, $scriptDir, $validatorConfig
153+
export scriptDir="$SCRIPT_DIR"
154+
export configDir="$GENESIS_DIR"
155+
export dataDir="$PROJECT_ROOT/shadow.data/hosts/$hostname"
156+
export validatorConfig="$VALIDATOR_CONFIG"
157+
158+
# Source parse-vc.sh to extract per-node config (quicPort, metricsPort, apiPort, etc.)
159+
# parse-vc.sh uses $item and $configDir
160+
source "$SCRIPT_DIR/parse-vc.sh"
161+
162+
# Source client-cmd.sh to get node_binary
163+
node_setup="binary"
164+
client_cmd="$SCRIPT_DIR/client-cmds/${client}-cmd.sh"
165+
if [ ! -f "$client_cmd" ]; then
166+
echo "❌ Error: Client command script not found: $client_cmd"
167+
echo " Available clients:"
168+
ls "$SCRIPT_DIR/client-cmds/"*-cmd.sh 2>/dev/null | sed 's/.*\// /' | sed 's/-cmd.sh//'
169+
exit 1
170+
fi
171+
source "$client_cmd"
172+
173+
# node_binary is now set by the client-cmd.sh script
174+
# Convert relative paths to absolute paths for Shadow
175+
# Extract the binary path (first word) and args (rest)
176+
binary_path=$(echo "$node_binary" | awk '{print $1}')
177+
binary_args=$(echo "$node_binary" | sed "s|^[^ ]*||")
178+
179+
# Make binary path absolute
180+
if [[ "$binary_path" != /* ]]; then
181+
binary_path="$(cd "$(dirname "$binary_path")" 2>/dev/null && pwd)/$(basename "$binary_path")" 2>/dev/null || binary_path="$PROJECT_ROOT/${binary_path#./}"
182+
fi
183+
184+
# Make all path args absolute: replace $configDir, $dataDir references with absolute paths
185+
# The client-cmd.sh already uses $configDir and $dataDir which we set to absolute paths
186+
187+
# Write host entry
188+
cat >> "$OUTPUT_FILE" << EOF
189+
$hostname:
190+
network_node_id: 0
191+
ip_addr: $ip
192+
processes:
193+
- path: $binary_path
194+
args: >-
195+
$binary_args
196+
start_time: 1s
197+
expected_final_state: running
198+
199+
EOF
200+
201+
echo "$item$hostname ($ip) [$client]"
202+
done
203+
204+
echo ""
205+
echo "📄 Shadow config written to: $OUTPUT_FILE"
206+
echo " Stop time: $STOP_TIME"
207+
echo " Nodes: $node_count"

0 commit comments

Comments
 (0)