Skip to content

feat: add Shadow network simulator automation#151

Open
GrapeBaBa wants to merge 6 commits intomainfrom
feat/shadow-automation
Open

feat: add Shadow network simulator automation#151
GrapeBaBa wants to merge 6 commits intomainfrom
feat/shadow-automation

Conversation

@GrapeBaBa
Copy link
Copy Markdown
Member

Summary

  • Add client-agnostic Shadow network simulator automation scripts
  • run-shadow.sh orchestrates genesis generation, shadow.yaml creation, and shadow execution
  • generate-shadow-yaml.sh reads validator-config.yaml and sources each client's client-cmds/<client>-cmd.sh to emit shadow.yaml
  • shadow-devnet/genesis/validator-config.yaml provides a 4-node Shadow config with virtual IPs (100.0.0.x)
  • client-cmds/leanspec-cmd.sh adds leanSpec client support
  • generate-genesis.sh gains --genesis-time <timestamp> flag for fixed timestamps (Shadow uses epoch 946684860)

Usage

# From any client repo with lean-quickstart as submodule:
cd lean-quickstart
./run-shadow.sh

The scripts auto-detect which clients are configured in shadow-devnet/genesis/validator-config.yaml and source matching client-cmds/<name>-cmd.sh files.

Test plan

  • Run ./run-shadow.sh on a Codespace with Shadow installed
  • Verify 4 nodes reach consensus (finalized slot > 0)
  • Verify generate-genesis.sh --genesis-time 946684860 produces correct fixed-time genesis

Copilot AI review requested due to automatic review settings April 2, 2026 09:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.sh to orchestrate genesis generation, Shadow config generation, and Shadow execution.
  • Added generate-shadow-yaml.sh to emit shadow.yaml by sourcing client-cmds/<client>-cmd.sh and parsing validator-config.yaml.
  • Extended generate-genesis.sh with --genesis-time <timestamp> to support fixed genesis timestamps (used by Shadow), plus added a Shadow-specific 4-node config and a new leanspec client 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.

Comment thread generate-shadow-yaml.sh
Comment on lines +100 to +104
# ========================================
# Read nodes from validator-config.yaml
# ========================================
node_names=($(yq eval '.validators[].name' "$VALIDATOR_CONFIG"))
node_count=${#node_names[@]}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread 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]}"

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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 uses AI. Check for mistakes.
Comment thread generate-shadow-yaml.sh
Comment on lines +179 to +181
# 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#./}"
Copy link

Copilot AI Apr 2, 2026

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.

Suggested change
# 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 uses AI. Check for mistakes.
Comment thread run-shadow.sh
STOP_TIME="$2"
shift 2
;;
--genesis-dir)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--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.

Suggested change
--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

Copilot uses AI. Check for mistakes.
Comment thread generate-shadow-yaml.sh
Comment on lines +162 to +166
# 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"
Copy link

Copilot AI Apr 2, 2026

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 uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 2, 2026 14:14
@GrapeBaBa GrapeBaBa force-pushed the feat/shadow-automation branch from 7c8fcf1 to d5b2769 Compare April 2, 2026 14:14
@GrapeBaBa GrapeBaBa force-pushed the feat/shadow-automation branch from d5b2769 to 2c695dd Compare April 2, 2026 14:15
- 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
@GrapeBaBa GrapeBaBa force-pushed the feat/shadow-automation branch from 2c695dd to a7c3b5b Compare April 2, 2026 14:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread generate-shadow-yaml.sh
Comment on lines +193 to +197
- path: $binary_path
args: >-
$binary_args
start_time: 1s
expected_final_state: running
Copy link

Copilot AI Apr 2, 2026

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).

Suggested change
- 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

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 16, 2026 15:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread generate-shadow-yaml.sh
Comment on lines +103 to +104
node_names=($(yq eval '.validators[].name' "$VALIDATOR_CONFIG"))
node_count=${#node_names[@]}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread run-shadow.sh
Comment on lines +62 to +64
--help|-h)
show_usage
;;
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread run-shadow.sh
Comment on lines +50 to +56
--stop-time)
STOP_TIME="$2"
shift 2
;;
--genesis-dir)
GENESIS_DIR="$(cd "$2" && pwd)"
shift 2
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread generate-shadow-yaml.sh
Comment on lines +141 to +143
# Extract client name from node prefix (zeam_0 → zeam, leanspec_0 → leanspec)
IFS='_' read -r -a elements <<< "$item"
client="${elements[0]}"
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread generate-shadow-yaml.sh
Comment on lines +173 to +177
# 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|^[^ ]*||")
Copy link

Copilot AI Apr 16, 2026

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 uses AI. Check for mistakes.
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.
Copilot AI review requested due to automatic review settings April 16, 2026 16:19
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).
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread generate-shadow-yaml.sh
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

Copy link

Copilot AI Apr 16, 2026

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.

Suggested change
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 uses AI. Check for mistakes.
Comment thread run-shadow.sh
Comment on lines +12 to +14
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread run-shadow.sh
Comment on lines +148 to +150
# Clean previous Shadow data
rm -rf "$PROJECT_ROOT/shadow.data"

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread run-shadow.sh
Comment on lines +37 to +39
EOF
exit 1
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread generate-genesis.sh
Comment on lines +537 to +538
echo " - attestation_pubkey: \"${PUBKEY_HEX#0x}\"" >> "$GENESIS_VALIDATORS_TMP"
echo " proposal_pubkey: \"${PUBKEY_HEX#0x}\"" >> "$GENESIS_VALIDATORS_TMP"
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread generate-shadow-yaml.sh
export scriptDir="$SCRIPT_DIR"
export configDir="$GENESIS_DIR"
export dataDir="$PROJECT_ROOT/shadow.data/hosts/$hostname"
export validatorConfig="$GENESIS_DIR"
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
export validatorConfig="$GENESIS_DIR"
export validatorConfig="$VALIDATOR_CONFIG"

Copilot uses AI. Check for mistakes.
Comment thread generate-shadow-yaml.sh
Comment on lines +101 to +105
# Read nodes from validator-config.yaml
# ========================================
node_names=($(yq eval '.validators[].name' "$VALIDATOR_CONFIG"))
node_count=${#node_names[@]}

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Shadow simulator may not follow symlinks correctly. Use cp instead
of ln -sf for the attester/proposer key file copies.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants