diff --git a/.github/workflows/run-invariants.yml b/.github/workflows/run-invariants.yml new file mode 100644 index 0000000000..0a9454e7be --- /dev/null +++ b/.github/workflows/run-invariants.yml @@ -0,0 +1,104 @@ +name: Run invariants on the local mainnet clone + +# Manual trigger only +on: + workflow_dispatch: + pull_request: + +permissions: + contents: write + packages: read + +concurrency: + group: run-invariants-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + localnet: + name: localnet + runs-on: [self-hosted, type-ccx43] + + steps: + # ------------------------------- + # Checkout repo + # ------------------------------- + - name: Checkout sources + uses: actions/checkout@v4 + + # ------------------------------- + # Install system dependencies + # ------------------------------- + - name: Install dependencies + run: | + sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update + sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" build-essential clang curl git make libssl-dev llvm libudev-dev protobuf-compiler pkg-config unzip + + # ------------------------------- + # Install Rust + # ------------------------------- + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + # ------------------------------- + # Cache Cargo + # ------------------------------- + - name: Utilize Shared Rust Cache + uses: Swatinem/rust-cache@v2 + with: + key: "run-invariants" + + # ------------------------------- + # Build Subtensor node + # ------------------------------- + - name: Build Subtensor node + run: cargo build --release -p node-subtensor + + # ------------------------------- + # Install Baedeker + # ------------------------------- + - name: Install Baedeker + run: ./scripts/invariants/install_baedeker.sh + + # ------------------------------- + # Login to GHCR + # ------------------------------- + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # ------------------------------- + # Generate chain spec + # ------------------------------- + - name: Generate chain spec + run: ./scripts/invariants/generate_spec.sh + + # ------------------------------- + # Start localnet nodes (background) + # ------------------------------- + - name: Start localnet nodes + run: | + ./scripts/invariants/run_localnet.sh ./target/release ./scripts/invariants/.bdk-env & + echo $! > /tmp/localnet.pid + sleep 1200 + + # ------------------------------- + # Wait until chain is producing blocks + # ------------------------------- + - name: Wait for chain to produce blocks + run: ./scripts/invariants/wait_for_chain.sh + + # ------------------------------- + # Pull JS RPC test image from GHCR + # ------------------------------- + + # ------------------------------- + # Run JS RPC tests + # ------------------------------- diff --git a/scripts/invariants/generate_spec.sh b/scripts/invariants/generate_spec.sh new file mode 100755 index 0000000000..88d52e0b21 --- /dev/null +++ b/scripts/invariants/generate_spec.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +SPECGEN_IMAGE="ghcr.io/opentensor/mainnet-genspec:latest" + +# ---------------------------------------- +# Check setup +# ---------------------------------------- + +command -v baedeker >/dev/null 2>&1 || { + echo "❌ baedeker is not installed" + exit 1 +} + +# ---------------------------------------- +# Paths +# ---------------------------------------- + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BDK_ENV_DIR="$SCRIPT_DIR/.bdk-env" +SECRET_DIR="$BDK_ENV_DIR/secret" + +# ---------------------------------------- +# Prepare .bdk-env structure +# ---------------------------------------- +echo "📁 Preparing .bdk-env directory structure..." +mkdir -p \ + "$BDK_ENV_DIR" \ + "$SECRET_DIR" \ + "$BDK_ENV_DIR/specs" + +# ---------------------------------------- +# Generate spec via baedeker +# ---------------------------------------- +echo "🚀 Storing state..." +docker run --rm \ + -v "$(command -v baedeker):/usr/local/bin/baedeker:ro" \ + -v "$BDK_ENV_DIR:/app/.bdk-env" \ + "$SPECGEN_IMAGE" + +# ---------------------------------------- +# Validate output +# ---------------------------------------- +SPEC_PATH="$BDK_ENV_DIR/specs/subtensor.json" + +if [[ ! -f "$SPEC_PATH" ]]; then + echo "❌ Expected spec not found: $SPEC_PATH" + exit 1 +fi + +echo "✅ Chain spec generated at:" +echo " $SPEC_PATH" \ No newline at end of file diff --git a/scripts/invariants/install_baedeker.sh b/scripts/invariants/install_baedeker.sh new file mode 100755 index 0000000000..79384ead1a --- /dev/null +++ b/scripts/invariants/install_baedeker.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION="v0.1.9" +BIN_URL="https://github.com/UniqueNetwork/baedeker/releases/download/${VERSION}/baedeker" +INSTALL_PATH="/usr/local/bin/baedeker" + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +echo "⬇️ Downloading baedeker ${VERSION}..." + +curl -fsSL "$BIN_URL" -o "$TMP_DIR/baedeker" + +chmod +x "$TMP_DIR/baedeker" + +sudo mv "$TMP_DIR/baedeker" "$INSTALL_PATH" + +echo "✅ baedeker installed at $INSTALL_PATH" + +baedeker version \ No newline at end of file diff --git a/scripts/invariants/run_localnet.sh b/scripts/invariants/run_localnet.sh new file mode 100755 index 0000000000..46372738b0 --- /dev/null +++ b/scripts/invariants/run_localnet.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail + +exec > >(awk '{ print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }') 2>&1 + +# ---------------------------------------- +# Args +# ---------------------------------------- + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " + exit 1 +fi + +BUILD_DIR="$(realpath "$1")" +BDK_ENV_DIR="$(realpath "$2")" + +BIN="$BUILD_DIR/node-subtensor" + +# ---------------------------------------- +# Derived paths +# ---------------------------------------- + +SPEC_PATH="$BDK_ENV_DIR/specs/subtensor.json" +SECRET_DIR="$BDK_ENV_DIR/secret" + +KEYSTORE_DIR="$SECRET_DIR/keystore" +NODE_KEY_DIR="$SECRET_DIR/node" + +# ---------------------------------------- +# Validation +# ---------------------------------------- + +[[ -x "$BIN" ]] || { echo "❌ node-subtensor not found: $BIN"; exit 1; } +[[ -f "$SPEC_PATH" ]] || { echo "❌ spec not found: $SPEC_PATH"; exit 1; } + +for d in "$KEYSTORE_DIR" "$NODE_KEY_DIR"; do + [[ -d "$d" ]] || { echo "❌ missing directory: $d"; exit 1; } +done + +# ---------------------------------------- +# Node commands +# ---------------------------------------- + +echo "🚀 Starting localnet nodes (Alice / Bob / Charlie)..." + +alice_start=( + "$BIN" + --base-path /tmp/alice + --chain="$SPEC_PATH" + --keystore-path="$KEYSTORE_DIR/subtensor-node-alice" + --node-key-file="$NODE_KEY_DIR/subtensor-node-alice" + --port 30334 + --rpc-port 9946 + --validator + --rpc-cors=all + --rpc-external + --unsafe-rpc-external + --rpc-methods=unsafe + --allow-private-ipv4 + --discover-local +) + +bob_start=( + "$BIN" + --base-path /tmp/bob + --chain="$SPEC_PATH" + --keystore-path="$KEYSTORE_DIR/subtensor-node-bob" + --node-key-file="$NODE_KEY_DIR/subtensor-node-bob" + --port 30335 + --rpc-port 9935 + --validator + --allow-private-ipv4 + --discover-local + --bootnodes /ip4/127.0.0.1/tcp/30334/p2p/12D3KooWMJ5Gmn2SPfx2TEFfvido1X8xhUZUnC2MbD2yTwKPQak8 +) + +charlie_start=( + "$BIN" + --base-path /tmp/charlie + --chain="$SPEC_PATH" + --keystore-path="$KEYSTORE_DIR/subtensor-node-charlie" + --node-key-file="$NODE_KEY_DIR/subtensor-node-charlie" + --port 30336 + --rpc-port 9936 + --validator + --allow-private-ipv4 + --discover-local + --bootnodes /ip4/127.0.0.1/tcp/30334/p2p/12D3KooWMJ5Gmn2SPfx2TEFfvido1X8xhUZUnC2MbD2yTwKPQak8 +) + +# ---------------------------------------- +# Launch (background, detached) +# ---------------------------------------- + +#("${alice_start[@]}" > /tmp/alice.log 2>&1 &) +#("${bob_start[@]}" > /tmp/bob.log 2>&1 &) +#("${charlie_start[@]}" > /tmp/charlie.log 2>&1 &) + +#echo "✅ Localnet started" +#echo " Logs:" +#echo " /tmp/alice.log" +#echo " /tmp/bob.log" +#echo " /tmp/charlie.log" + +# ---------------------------------------- +# Exit so CI can continue +# ---------------------------------------- + +#exit 0 +"${alice_start[@]}" +"${bob_start[@]}" +"${charlie_start[@]}" \ No newline at end of file diff --git a/scripts/invariants/run_localnet2.sh b/scripts/invariants/run_localnet2.sh new file mode 100755 index 0000000000..5676de797f --- /dev/null +++ b/scripts/invariants/run_localnet2.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ---------------------------------------- +# Args +# ---------------------------------------- + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " + exit 1 +fi + +BUILD_DIR="$(realpath "$1")" +BDK_ENV_DIR="$(realpath "$2")" + +BIN="$BUILD_DIR/node-subtensor" + +[[ -x "$BIN" ]] || { echo "❌ node-subtensor not found: $BIN"; exit 1; } + +echo "🚀 Starting localnet nodes (Alice / Bob / Charlie)..." + +alice_start=( + "$BIN" + --dev + --port 30334 + --rpc-port 9946 +) + +("${alice_start[@]}" > /tmp/alice.log 2>&1 &) + +echo "✅ Localnet started" +echo " Logs:" +echo " /tmp/alice.log" + +exit 0 \ No newline at end of file diff --git a/scripts/invariants/wait_for_chain.sh b/scripts/invariants/wait_for_chain.sh new file mode 100755 index 0000000000..cd89817472 --- /dev/null +++ b/scripts/invariants/wait_for_chain.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ------------------------------- +# Configurable via environment +# ------------------------------- + +RPC_HOST="${RPC_HOST:-127.0.0.1}" +RPC_PORT="${RPC_PORT:-9946}" +RPC_URL="http://${RPC_HOST}:${RPC_PORT}" + +MAX_RETRIES="${MAX_RETRIES:-30}" +SLEEP_INTERVAL="${SLEEP_INTERVAL:-2}" # seconds + +# ------------------------------- +# Wait for RPC availability +# ------------------------------- + +echo "Waiting for node RPC at $RPC_URL..." + +ps aux | grep node-subtensor + +for ((i=1; i<=MAX_RETRIES; i++)); do + if curl -sf -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"system_health","params":[],"id":1}' \ + "$RPC_URL" > /tmp/health.json; then + + PEERS=$(jq '.result.peers // 0' /tmp/health.json) + SYNCING=$(jq '.result.isSyncing // true' /tmp/health.json) + + echo "[Attempt $i/$MAX_RETRIES] Peers=$PEERS, Syncing=$SYNCING" + + if [[ "$PEERS" -gt 0 ]]; then + echo "✅ Node has peers connected" + break + fi + fi + + sleep "$SLEEP_INTERVAL" +done + +# Final check if peers never connected +PEERS=$(jq '.result.peers // 0' /tmp/health.json || echo 0) +if [[ "$PEERS" -le 0 ]]; then + echo "❌ Node failed to connect to any peers after $MAX_RETRIES retries" + exit 1 +fi + +# ------------------------------- +# Check chain height +# ------------------------------- + +echo "Checking chain height..." + +HEADER_JSON=$(curl -sf -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"chain_getHeader","params":[],"id":1}' \ + "$RPC_URL") + +HEIGHT_HEX=$(echo "$HEADER_JSON" | jq -r '.result.number') +HEIGHT=$((HEIGHT_HEX)) + +if [[ "$HEIGHT" -le 0 ]]; then + echo "❌ Chain is not progressing. Height=$HEIGHT" + exit 1 +fi + +echo "✅ Chain is producing blocks. Current height=$HEIGHT" \ No newline at end of file