Skip to content

Commit a7c3b5b

Browse files
committed
feat: add Shadow network simulator automation
- run-shadow.sh: orchestrator (genesis → shadow.yaml → shadow) - generate-shadow-yaml.sh: generates shadow.yaml from validator-config.yaml using each client's client-cmds/<client>-cmd.sh - shadow-devnet/genesis/validator-config.yaml: 4 zeam nodes with Shadow IPs - generate-genesis.sh: add --genesis-time flag for fixed timestamps - README: add Shadow Network Simulator section
1 parent 6baba7c commit a7c3b5b

5 files changed

Lines changed: 525 additions & 13 deletions

File tree

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,76 @@ Ansible mode is ideal for:
10991099
- Multi-host deployments
11001100
- Infrastructure as Code workflows
11011101

1102+
## Shadow Network Simulator
1103+
1104+
[Shadow](https://shadow.github.io/) is a discrete-event network simulator that runs real application binaries over a simulated network. It is useful for reproducible multi-node testing without real hardware or cloud resources.
1105+
1106+
### Requirements
1107+
1108+
- **Shadow** (v3.x): [Install guide](https://shadow.github.io/docs/guide/install.html)
1109+
- **yq**: YAML processor (same as regular devnet)
1110+
- **Docker**: Required for genesis generation
1111+
1112+
### Quick Start
1113+
1114+
From the client repo root (with lean-quickstart as a submodule):
1115+
1116+
```sh
1117+
cd lean-quickstart
1118+
./run-shadow.sh
1119+
```
1120+
1121+
This single command:
1122+
1. Generates genesis with a fixed Shadow-compatible timestamp (`946684860`)
1123+
2. Generates `shadow.yaml` from `shadow-devnet/genesis/validator-config.yaml`
1124+
3. Runs Shadow simulation (default: 360 seconds)
1125+
1126+
### Options
1127+
1128+
```sh
1129+
# Custom simulation time
1130+
./run-shadow.sh --stop-time 600s
1131+
1132+
# Force regenerate hash-sig keys
1133+
./run-shadow.sh --forceKeyGen
1134+
1135+
# Use a different genesis directory
1136+
./run-shadow.sh --genesis-dir /path/to/custom/genesis
1137+
```
1138+
1139+
### Configuration
1140+
1141+
Shadow devnet configuration lives in `shadow-devnet/genesis/validator-config.yaml`. It uses virtual IPs (`100.0.0.x`) required by Shadow's simulated network:
1142+
1143+
```yaml
1144+
validators:
1145+
- name: "zeam_0"
1146+
enrFields:
1147+
ip: "100.0.0.1"
1148+
quic: 9001
1149+
# ...
1150+
```
1151+
1152+
To test a different client, change the node name prefixes (e.g., `ream_0`) and ensure a matching `client-cmds/<client>-cmd.sh` exists.
1153+
1154+
### How It Works
1155+
1156+
- **`run-shadow.sh`** — orchestrator: genesis → shadow.yaml → `shadow` execution
1157+
- **`generate-shadow-yaml.sh`** — reads `validator-config.yaml`, sources each client's `client-cmds/<client>-cmd.sh` to build per-node command lines, and emits `shadow.yaml`
1158+
- **`generate-genesis.sh --genesis-time 946684860`** — produces genesis with Shadow's virtual clock epoch (Jan 1, 2000 + 60s warmup)
1159+
1160+
Shadow's virtual clock starts at Unix timestamp `946684800` (Jan 1, 2000). Genesis time is set to `946684860` (epoch + 60s) to give nodes time to initialize.
1161+
1162+
### Checking Results
1163+
1164+
```sh
1165+
# Check consensus status
1166+
grep 'new_head\|finalized' shadow.data/hosts/*/*.stderr | tail -20
1167+
1168+
# Full node logs
1169+
cat shadow.data/hosts/zeam-0/zeam.1000.stderr
1170+
```
1171+
11021172
## Client branches
11031173

11041174
Clients can maintain their own branches to integrated and use binay with their repos as the static targets (check `git diff main zeam_repo`, it has two nodes, both specified to run `zeam` for sim testing in zeam using the quickstart generated genesis).

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)