feat: add Shadow network simulator automation#151
Conversation
There was a problem hiding this comment.
Pull request overview
Adds automation to run a multi-node devnet inside the Shadow network simulator, including fixed-time genesis generation and client-agnostic Shadow config generation based on validator-config.yaml.
Changes:
- Added
run-shadow.shto orchestrate genesis generation, Shadow config generation, and Shadow execution. - Added
generate-shadow-yaml.shto emitshadow.yamlby sourcingclient-cmds/<client>-cmd.shand parsingvalidator-config.yaml. - Extended
generate-genesis.shwith--genesis-time <timestamp>to support fixed genesis timestamps (used by Shadow), plus added a Shadow-specific 4-node config and a newleanspecclient cmd.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| shadow-devnet/genesis/validator-config.yaml | Adds a 4-node Shadow validator config with virtual IPs and ports. |
| run-shadow.sh | New top-level runner script to generate genesis + shadow.yaml and run Shadow. |
| generate-shadow-yaml.sh | New generator that converts validator config + client command scripts into shadow.yaml. |
| generate-genesis.sh | Adds support for exact genesis timestamps via --genesis-time. |
| client-cmds/leanspec-cmd.sh | Adds leanspec client command definitions for quickstart automation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # ======================================== | ||
| # Read nodes from validator-config.yaml | ||
| # ======================================== | ||
| node_names=($(yq eval '.validators[].name' "$VALIDATOR_CONFIG")) | ||
| node_count=${#node_names[@]} |
There was a problem hiding this comment.
yq is used to read validator names before any dependency check, so if yq is missing this script will fail with a generic "command not found". Add an explicit command -v yq check (similar to parse-vc.sh) near the top and exit with a clear install hint.
| # Extract client name from node prefix (zeam_0 → zeam, leanspec_0 → leanspec) | ||
| IFS='_' read -r -a elements <<< "$item" | ||
| client="${elements[0]}" | ||
|
|
There was a problem hiding this comment.
The client value is derived from validator-config.yaml and then interpolated into a path that is sourced. If a user points --genesis-dir at an untrusted config, a crafted validator name could trigger path traversal (e.g., ../../...) and execute arbitrary code. Restrict client to a safe allowlist pattern (e.g., [a-z0-9-]+), reject values containing / or .., and fail fast with an error before sourcing.
| # 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 |
| # 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#./}" |
There was a problem hiding this comment.
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 |
| STOP_TIME="$2" | ||
| shift 2 | ||
| ;; | ||
| --genesis-dir) |
There was a problem hiding this comment.
--genesis-dir is resolved via cd "$2" && pwd under set -e. If the path is missing/invalid, the script exits with a raw cd error rather than a user-friendly message. Add an explicit directory existence check and print a clear error (including the provided value) before exiting.
| --genesis-dir) | |
| --genesis-dir) | |
| if [ -z "$2" ]; then | |
| echo "❌ Error: --genesis-dir requires a path argument." >&2 | |
| exit 1 | |
| fi | |
| if [ ! -d "$2" ]; then | |
| echo "❌ Error: Genesis directory '$2' does not exist or is not a directory." >&2 | |
| exit 1 | |
| fi |
| # Source client-cmd.sh to get node_binary | ||
| node_setup="binary" | ||
| client_cmd="$SCRIPT_DIR/client-cmds/${client}-cmd.sh" | ||
| if [ ! -f "$client_cmd" ]; then | ||
| echo "❌ Error: Client command script not found: $client_cmd" |
There was a problem hiding this comment.
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.
7c8fcf1 to
d5b2769
Compare
d5b2769 to
2c695dd
Compare
- 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
2c695dd to
a7c3b5b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - path: $binary_path | ||
| args: >- | ||
| $binary_args | ||
| start_time: 1s | ||
| expected_final_state: running |
There was a problem hiding this comment.
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.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| node_names=($(yq eval '.validators[].name' "$VALIDATOR_CONFIG")) | ||
| node_count=${#node_names[@]} |
There was a problem hiding this comment.
This script calls yq before any dependency validation (e.g., when building node_names). If generate-shadow-yaml.sh is run directly and yq is missing, it will fail with a generic command-not-found rather than a clear error. Consider adding an explicit command -v yq check near the top (similar to parse-vc.sh / generate-genesis.sh) before the first yq eval invocation.
| --help|-h) | ||
| show_usage | ||
| ;; |
There was a problem hiding this comment.
show_usage always exits with status 1, and --help|-h calls show_usage, so ./run-shadow.sh --help currently returns a non-zero exit code. Consider exiting 0 for help output (e.g., have show_usage accept an exit code, or handle --help|-h separately).
| --stop-time) | ||
| STOP_TIME="$2" | ||
| shift 2 | ||
| ;; | ||
| --genesis-dir) | ||
| GENESIS_DIR="$(cd "$2" && pwd)" | ||
| shift 2 |
There was a problem hiding this comment.
Argument parsing for --stop-time and --genesis-dir assumes $2 exists. If a user passes --stop-time (or --genesis-dir) as the last argument, STOP_TIME/GENESIS_DIR will be set to empty and shift 2 will run, leading to confusing downstream failures (or an immediate exit due to set -e on the cd). Consider validating that $2 is present and not another flag before consuming it (as done in generate-shadow-yaml.sh).
| # Extract client name from node prefix (zeam_0 → zeam, leanspec_0 → leanspec) | ||
| IFS='_' read -r -a elements <<< "$item" | ||
| client="${elements[0]}" |
There was a problem hiding this comment.
PR description mentions adding client-cmds/leanspec-cmd.sh for leanSpec support, but there is no such script present in the repo contents. Either add the missing client-cmds/leanspec-cmd.sh (and update shadow-devnet/genesis/validator-config.yaml/docs accordingly), or remove/adjust the leanSpec references so the automation matches what’s actually included.
| # node_binary is now set by the client-cmd.sh script | ||
| # Convert relative paths to absolute paths for Shadow | ||
| # Extract the binary path (first word) and args (rest) | ||
| binary_path=$(echo "$node_binary" | awk '{print $1}') | ||
| binary_args=$(echo "$node_binary" | sed "s|^[^ ]*||") |
There was a problem hiding this comment.
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.
zeam expects --validator_config to be a directory containing validator-config.yaml, not the yaml file path itself.
… pubkeys The latest zeam (after main merge) expects GENESIS_VALIDATORS entries to be maps with attestation_pubkey and proposal_pubkey fields, not plain hex strings.
Latest zeam expects annotated_validators.yaml to have separate attester and proposer entries per validator. Since both roles use the same hash-sig key, create symlinks with the expected naming (validator_N_attester_sk.ssz / validator_N_proposer_sk.ssz).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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#./}" | ||
| fi | ||
|
|
There was a problem hiding this comment.
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 |
| SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
|
|
There was a problem hiding this comment.
PROJECT_ROOT is set to the parent of SCRIPT_DIR. If run-shadow.sh is executed from a standalone lean-quickstart checkout (not as a submodule), this points outside the repo, and later steps will write/delete files in the parent directory. Consider defaulting PROJECT_ROOT to SCRIPT_DIR (and/or adding an override flag) and/or validating that the parent actually contains the expected client repo layout before using it.
| # Clean previous Shadow data | ||
| rm -rf "$PROJECT_ROOT/shadow.data" | ||
|
|
There was a problem hiding this comment.
rm -rf "$PROJECT_ROOT/shadow.data" is destructive, and combined with PROJECT_ROOT being the parent of SCRIPT_DIR it can delete data outside the repo when the script is run from an unexpected location. Add a safety guard (e.g., ensure the path is within the intended workspace and not //$HOME/etc., or require an explicit --project-root confirmation) before removing.
| EOF | ||
| exit 1 | ||
| } |
There was a problem hiding this comment.
show_usage exits with status 1 unconditionally, so --help also returns a failure code. Consider returning exit code 0 for --help (and keeping non-zero exit codes for invalid usage) to match common CLI behavior.
| echo " - attestation_pubkey: \"${PUBKEY_HEX#0x}\"" >> "$GENESIS_VALIDATORS_TMP" | ||
| echo " proposal_pubkey: \"${PUBKEY_HEX#0x}\"" >> "$GENESIS_VALIDATORS_TMP" |
There was a problem hiding this comment.
GENESIS_VALIDATORS in config.yaml is documented (and previously generated) as a list of pubkey strings, but this change writes a list of objects with attestation_pubkey / proposal_pubkey. This is a breaking change for any client tooling that parses GENESIS_VALIDATORS as a string list (as described in README). Keep the existing string list format, or introduce a new separate field for the expanded structure while preserving backward compatibility.
| export scriptDir="$SCRIPT_DIR" | ||
| export configDir="$GENESIS_DIR" | ||
| export dataDir="$PROJECT_ROOT/shadow.data/hosts/$hostname" | ||
| export validatorConfig="$GENESIS_DIR" |
There was a problem hiding this comment.
validatorConfig is exported as the genesis directory path, but the repo/README treat validator_config as a path (or the special value genesis_bootnode). For clients like zeam-cmd.sh that pass --validator_config $validatorConfig, a directory here is likely incorrect. Set validatorConfig to the intended value (e.g., $GENESIS_DIR/validator-config.yaml or genesis_bootnode) and keep it consistent with spin-node.sh semantics.
| export validatorConfig="$GENESIS_DIR" | |
| export validatorConfig="$VALIDATOR_CONFIG" |
| # Read nodes from validator-config.yaml | ||
| # ======================================== | ||
| node_names=($(yq eval '.validators[].name' "$VALIDATOR_CONFIG")) | ||
| node_count=${#node_names[@]} | ||
|
|
There was a problem hiding this comment.
This script invokes yq before sourcing parse-vc.sh (which does the dependency check), so if yq isn't installed the failure will be a generic "command not found". Add an explicit yq presence check near the top (similar to parse-vc.sh / generate-genesis.sh) so running generate-shadow-yaml.sh directly produces a clear error message.
Shadow simulator may not follow symlinks correctly. Use cp instead of ln -sf for the attester/proposer key file copies.
Summary
run-shadow.shorchestrates genesis generation, shadow.yaml creation, and shadow executiongenerate-shadow-yaml.shreadsvalidator-config.yamland sources each client'sclient-cmds/<client>-cmd.shto emitshadow.yamlshadow-devnet/genesis/validator-config.yamlprovides a 4-node Shadow config with virtual IPs (100.0.0.x)client-cmds/leanspec-cmd.shadds leanSpec client supportgenerate-genesis.shgains--genesis-time <timestamp>flag for fixed timestamps (Shadow uses epoch946684860)Usage
The scripts auto-detect which clients are configured in
shadow-devnet/genesis/validator-config.yamland source matchingclient-cmds/<name>-cmd.shfiles.Test plan
./run-shadow.shon a Codespace with Shadow installedgenerate-genesis.sh --genesis-time 946684860produces correct fixed-time genesis