-
Notifications
You must be signed in to change notification settings - Fork 25
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 all commits
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[@]} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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]}" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| # 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 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 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.