Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .github/workflows/run-invariants.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Run invariants on the local mainnet clone

# Manual trigger only
on:
workflow_dispatch:
pull_request:

permissions:
contents: 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-ccx33]
permissions:
contents: write

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/run_localnet.sh ./subtensor/target/release ./.bdk-env/specs/subtensor.json &
echo $! > /tmp/localnet.pid
sleep 5

# -------------------------------
# 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
# -------------------------------
52 changes: 52 additions & 0 deletions scripts/invariants/generate_spec.sh
Original file line number Diff line number Diff line change
@@ -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"
21 changes: 21 additions & 0 deletions scripts/invariants/install_baedeker.sh
Original file line number Diff line number Diff line change
@@ -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
97 changes: 97 additions & 0 deletions scripts/invariants/run_localnet.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail

# -------------------------------
# Usage
# -------------------------------
# ./run_localnet.sh <BUILD_DIRECTORY> <SPEC_PATH>
# Example:
# ./run_localnet.sh ./subtensor/target/release ./.bdk-env/specs/subtensor.json
# -------------------------------

BUILD_DIR="${1:?Build directory missing}"
SPEC_PATH="${2:?Spec path missing}"

BIN="${BUILD_DIR}/node-subtensor"

# -------------------------------
# Purge previous chain state
# -------------------------------
echo "*** Purging previous state..."

for NODE in alice bob charlie; do
"$BIN" purge-chain -y --base-path "/tmp/$NODE" --chain="$SPEC_PATH" >/dev/null 2>&1
done

echo "*** Previous chain state purged"

# -------------------------------
# Define nodes
# -------------------------------
ALICE_BASE="/tmp/alice"
BOB_BASE="/tmp/bob"
CHARLIE_BASE="/tmp/charlie"

alice_start=(
"$BIN"
--base-path "$ALICE_BASE"
--chain="$SPEC_PATH"
--keystore-path="./.bdk-env/secret/keystore/subtensor-node-alice"
--node-key-file="./.bdk-env/secret/node/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 "$BOB_BASE"
--chain="$SPEC_PATH"
--keystore-path="./.bdk-env/secret/keystore/subtensor-node-bob"
--node-key-file="./.bdk-env/secret/node/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 "$CHARLIE_BASE"
--chain="$SPEC_PATH"
--keystore-path="./.bdk-env/secret/keystore/subtensor-node-charlie"
--node-key-file="./.bdk-env/secret/node/subtensor-node-charlie"
--port 30336
--rpc-port 9936
--validator
--allow-private-ipv4
--discover-local
--bootnodes /ip4/127.0.0.1/tcp/30334/p2p/12D3KooWMJ5Gmn2SPfx2TEFfvido1X8xhUZUnC2MbD2yTwKPQak8
)

# -------------------------------
# Start nodes in background
# -------------------------------

echo "*** Starting localnet nodes (Alice/Bob/Charlie)..."
echo "Press Ctrl+C to terminate"

# trap ensures all background nodes are killed if script is interrupted
trap 'kill 0' SIGINT

# Run nodes concurrently
("${alice_start[@]}" 2>&1 &)
("${bob_start[@]}" 2>&1 &)
("${charlie_start[@]}" 2>&1 &)

# Keep script alive to allow external checks / JS tests
# CI runner will terminate at job end
sleep infinity
65 changes: 65 additions & 0 deletions scripts/invariants/wait_for_chain.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/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..."

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"
Loading