-
Notifications
You must be signed in to change notification settings - Fork 28
feat: add Shadow network simulator automation #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
a7c3b5b
190728b
b4f2459
8352484
79ce077
7867518
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,207 @@ | ||||||||||||||||||||||||||||||||||
| #!/bin/bash | ||||||||||||||||||||||||||||||||||
| set -e | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # generate-shadow-yaml.sh — Generate shadow.yaml from validator-config.yaml | ||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||
| # Multi-client: reuses existing client-cmds/<client>-cmd.sh to get node_binary. | ||||||||||||||||||||||||||||||||||
| # Works for zeam, ream, lantern, gean, or any client with a *-cmd.sh file. | ||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||
| # Usage: | ||||||||||||||||||||||||||||||||||
| # ./generate-shadow-yaml.sh <genesis-dir> --project-root <path> [--stop-time 360s] [--output shadow.yaml] | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| show_usage() { | ||||||||||||||||||||||||||||||||||
| cat << EOF | ||||||||||||||||||||||||||||||||||
| Usage: $0 <genesis-dir> --project-root <path> [--stop-time 360s] [--output shadow.yaml] | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Generate a Shadow network simulator configuration (shadow.yaml) from validator-config.yaml. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Arguments: | ||||||||||||||||||||||||||||||||||
| genesis-dir Path to genesis directory containing validator-config.yaml | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Options: | ||||||||||||||||||||||||||||||||||
| --project-root <path> Project root directory (parent of lean-quickstart). Required. | ||||||||||||||||||||||||||||||||||
| --stop-time <time> Shadow simulation stop time (default: 360s) | ||||||||||||||||||||||||||||||||||
| --output <path> Output shadow.yaml path (default: <project-root>/shadow.yaml) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| This script is client-agnostic. It reads node names from validator-config.yaml, | ||||||||||||||||||||||||||||||||||
| extracts the client name from the node prefix (e.g., zeam_0 → zeam), and sources | ||||||||||||||||||||||||||||||||||
| the corresponding client-cmds/<client>-cmd.sh to generate per-node arguments. | ||||||||||||||||||||||||||||||||||
| EOF | ||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # ======================================== | ||||||||||||||||||||||||||||||||||
| # Parse arguments | ||||||||||||||||||||||||||||||||||
| # ======================================== | ||||||||||||||||||||||||||||||||||
| if [ -z "$1" ] || [ "${1:0:1}" == "-" ]; then | ||||||||||||||||||||||||||||||||||
| show_usage | ||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| GENESIS_DIR="$(cd "$1" && pwd)" | ||||||||||||||||||||||||||||||||||
| shift | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| PROJECT_ROOT="" | ||||||||||||||||||||||||||||||||||
| STOP_TIME="360s" | ||||||||||||||||||||||||||||||||||
| OUTPUT_FILE="" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| while [[ $# -gt 0 ]]; do | ||||||||||||||||||||||||||||||||||
| case "$1" in | ||||||||||||||||||||||||||||||||||
| --project-root) | ||||||||||||||||||||||||||||||||||
| if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then | ||||||||||||||||||||||||||||||||||
| PROJECT_ROOT="$(cd "$2" && pwd)" | ||||||||||||||||||||||||||||||||||
| shift 2 | ||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||
| echo "❌ Error: --project-root requires a path" | ||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
| ;; | ||||||||||||||||||||||||||||||||||
| --stop-time) | ||||||||||||||||||||||||||||||||||
| if [ -n "$2" ]; then | ||||||||||||||||||||||||||||||||||
| STOP_TIME="$2" | ||||||||||||||||||||||||||||||||||
| shift 2 | ||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||
| echo "❌ Error: --stop-time requires a value" | ||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
| ;; | ||||||||||||||||||||||||||||||||||
| --output) | ||||||||||||||||||||||||||||||||||
| if [ -n "$2" ]; then | ||||||||||||||||||||||||||||||||||
| OUTPUT_FILE="$2" | ||||||||||||||||||||||||||||||||||
| shift 2 | ||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||
| echo "❌ Error: --output requires a path" | ||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
| ;; | ||||||||||||||||||||||||||||||||||
| *) | ||||||||||||||||||||||||||||||||||
| echo "❌ Unknown option: $1" | ||||||||||||||||||||||||||||||||||
| show_usage | ||||||||||||||||||||||||||||||||||
| ;; | ||||||||||||||||||||||||||||||||||
| esac | ||||||||||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if [ -z "$PROJECT_ROOT" ]; then | ||||||||||||||||||||||||||||||||||
| echo "❌ Error: --project-root is required" | ||||||||||||||||||||||||||||||||||
| show_usage | ||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if [ -z "$OUTPUT_FILE" ]; then | ||||||||||||||||||||||||||||||||||
| OUTPUT_FILE="$PROJECT_ROOT/shadow.yaml" | ||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| VALIDATOR_CONFIG="$GENESIS_DIR/validator-config.yaml" | ||||||||||||||||||||||||||||||||||
| if [ ! -f "$VALIDATOR_CONFIG" ]; then | ||||||||||||||||||||||||||||||||||
| echo "❌ Error: validator-config.yaml not found at $VALIDATOR_CONFIG" | ||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # ======================================== | ||||||||||||||||||||||||||||||||||
| # Read nodes from validator-config.yaml | ||||||||||||||||||||||||||||||||||
| # ======================================== | ||||||||||||||||||||||||||||||||||
| node_names=($(yq eval '.validators[].name' "$VALIDATOR_CONFIG")) | ||||||||||||||||||||||||||||||||||
| node_count=${#node_names[@]} | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+103
to
+104
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+101
to
+105
|
||||||||||||||||||||||||||||||||||
| if [ "$node_count" -eq 0 ]; then | ||||||||||||||||||||||||||||||||||
| echo "❌ Error: No validators found in $VALIDATOR_CONFIG" | ||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| echo "🔧 Generating shadow.yaml for $node_count nodes..." | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # ======================================== | ||||||||||||||||||||||||||||||||||
| # Write shadow.yaml preamble | ||||||||||||||||||||||||||||||||||
| # ======================================== | ||||||||||||||||||||||||||||||||||
| cat > "$OUTPUT_FILE" << EOF | ||||||||||||||||||||||||||||||||||
| # Auto-generated Shadow network simulator configuration | ||||||||||||||||||||||||||||||||||
| # Generated from: $VALIDATOR_CONFIG | ||||||||||||||||||||||||||||||||||
| # Nodes: ${node_names[*]} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| general: | ||||||||||||||||||||||||||||||||||
| model_unblocked_syscall_latency: true | ||||||||||||||||||||||||||||||||||
| stop_time: $STOP_TIME | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| experimental: | ||||||||||||||||||||||||||||||||||
| native_preemption_enabled: true | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| network: | ||||||||||||||||||||||||||||||||||
| graph: | ||||||||||||||||||||||||||||||||||
| type: 1_gbit_switch | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| hosts: | ||||||||||||||||||||||||||||||||||
| EOF | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # ======================================== | ||||||||||||||||||||||||||||||||||
| # Generate per-node host entries | ||||||||||||||||||||||||||||||||||
| # ======================================== | ||||||||||||||||||||||||||||||||||
| for i in "${!node_names[@]}"; do | ||||||||||||||||||||||||||||||||||
| item="${node_names[$i]}" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Extract client name from node prefix (zeam_0 → zeam, leanspec_0 → leanspec) | ||||||||||||||||||||||||||||||||||
| IFS='_' read -r -a elements <<< "$item" | ||||||||||||||||||||||||||||||||||
| client="${elements[0]}" | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+141
to
+143
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| # Validate client name to prevent path traversal and restrict characters | |
| # Allowed: lowercase letters, digits, and hyphens. Disallow '/' and '..'. | |
| if [[ "$client" == *"/"* || "$client" == *".."* || ! "$client" =~ ^[a-z0-9-]+$ ]]; then | |
| echo "❌ Error: Invalid client name '$client'. Allowed pattern: [a-z0-9-]+ and no '/' or '..'." | |
| exit 1 | |
| fi |
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This script sources each client-cmds/*-cmd.sh, which (per existing quickstart contract) sets node_setup and provides both node_binary and node_docker. However, the generated Shadow config always uses node_binary later, even when node_setup="docker" (e.g., leanspec-cmd.sh explicitly selects docker). Either honor node_setup (and build an executable command accordingly), or fail fast with a clear error if a client is configured for docker-only so Shadow runs don’t silently generate a non-working config.
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
node_binary from existing client-cmds/*-cmd.sh scripts is formatted for eval (multi-line with trailing \ line continuations and sometimes escaped quotes). Writing the raw remainder into shadow.yaml via a folded block (>-) turns \<newline> into \ , which changes argument tokenization and will likely break client startup under Shadow. Suggest normalizing node_binary before splitting (e.g., strip end-of-line \ continuations and join lines), or switch to emitting an explicit YAML args list derived from a properly tokenized argv representation rather than a shell-formatted string.
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic that makes binary_path absolute treats any command that doesn't start with / as a filesystem path. This breaks PATH-resolved commands like uv (used by leanspec-cmd.sh), turning it into <cwd>/uv which likely doesn't exist. Only absolutize when the command contains a / (i.e., is a path), or resolve bare commands via command -v and keep them unchanged if found on PATH.
| # Make binary path absolute | |
| if [[ "$binary_path" != /* ]]; then | |
| binary_path="$(cd "$(dirname "$binary_path")" 2>/dev/null && pwd)/$(basename "$binary_path")" 2>/dev/null || binary_path="$PROJECT_ROOT/${binary_path#./}" | |
| # Make binary path absolute when it is a filesystem path. | |
| # - If binary_path starts with '/', it is already absolute. | |
| # - If binary_path contains '/', treat it as a path and absolutize it. | |
| # - If binary_path has no '/', treat it as a bare command and leave it for PATH resolution. | |
| if [[ "$binary_path" != /* ]]; then | |
| if [[ "$binary_path" == */* ]]; then | |
| binary_path="$(cd "$(dirname "$binary_path")" 2>/dev/null && pwd)/$(basename "$binary_path")" 2>/dev/null || binary_path="$PROJECT_ROOT/${binary_path#./}" | |
| else | |
| # Bare command: verify it exists on PATH but do not rewrite it into a filesystem path. | |
| if ! command -v "$binary_path" >/dev/null 2>&1; then | |
| echo "⚠️ Warning: binary '$binary_path' not found on PATH; Shadow may fail to start this process." >&2 | |
| fi | |
| fi |
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After resolving binary_path, the script should verify the file exists and is executable before writing shadow.yaml. Add an [[ -x "$binary_path" ]] (or similar) check and fail with a clear error so users don’t discover missing builds only after starting Shadow.
| if [[ ! -x "$binary_path" ]]; then | |
| echo "❌ Error: Resolved binary for node '$item' (client '$client') is missing or not executable: $binary_path" | |
| echo " Build the client first or fix the path returned by $client_cmd." | |
| exit 1 | |
| fi |
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generated YAML under processes: is incorrectly indented (- path is aligned with processes:). This will produce invalid YAML (or an unexpected structure) for Shadow. Indent the process list items under processes: (and their nested keys accordingly).
| - path: $binary_path | |
| args: >- | |
| $binary_args | |
| start_time: 1s | |
| expected_final_state: running | |
| - path: $binary_path | |
| args: >- | |
| $binary_args | |
| start_time: 1s | |
| expected_final_state: running |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yqis used to read validator names before any dependency check, so ifyqis missing this script will fail with a generic "command not found". Add an explicitcommand -v yqcheck (similar toparse-vc.sh) near the top and exit with a clear install hint.