diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8a5f200 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + pull_request: + workflow_dispatch: + +jobs: + dry-run: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y zstd stress + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: brew install zstd stress + + - name: Verify Makefile parses and dependency check passes + run: make -n up + + # Linux-only: macOS runners lack Docker. The Darwin Makefile branch is + # already covered by dry-run; the live stack itself is OS-independent. + live-run: + needs: dry-run + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y zstd stress jq + + - name: Boot network from chainstate archive + run: make up + + - name: Wait for network to be live + run: | + for i in $(seq 1 30); do + if make test; then + echo "Network is live" + exit 0 + fi + echo "Attempt $i/30 — not ready yet, sleeping 10s" + sleep 10 + done + echo "Network did not become live within timeout" + exit 1 + + - name: Tear down + if: always() + run: make down-force diff --git a/Makefile b/Makefile index 67d9773..a871ad8 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,34 @@ -# List of binaries hacknet needs to function properly -COMMANDS := sudo tar zstd getent stress -$(foreach bin,$(COMMANDS),\ - $(if $(shell command -v $(bin) 2> /dev/null),$(info),$(error Missing required dependency: `$(bin)`))) +# OS Detection and Cross-Platform Support +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + OS := macos + export UID := $(shell id -u) + export GID := $(shell id -g) + # macOS: use sysctl for CPU count + STRESS_CORES ?= $(shell sysctl -n hw.ncpu) + # List of binaries hacknet needs to function properly + COMMANDS := sudo tar zstd stress + # macOS Docker Desktop maps host UID into its VM; running BSD tar via sudo + # restores archive ownership and leaves bind-mount sources unwritable. + # Extract as the current user so everything lands user-owned. + TAR_EXTRACT := tar -xf +else + OS := linux + # Linux: use getent + export UID := $(shell getent passwd $$(whoami) | cut -d":" -f 3) + export GID := $(shell getent passwd $$(whoami) | cut -d":" -f 4) + # Linux: use /proc/cpuinfo for CPU count + STRESS_CORES ?= $(shell cat /proc/cpuinfo | grep processor | wc -l) + # List of binaries hacknet needs to function properly + COMMANDS := sudo tar zstd getent stress + TAR_EXTRACT := sudo tar --same-owner -xf +endif + +# Verify required dependencies exist +$(foreach bin,$(COMMANDS),$(if $(shell command -v $(bin) 2> /dev/null),$(info),$(error Missing required dependency: `$(bin)`))) TARGET := $(firstword $(MAKECMDGOALS)) PARAMS := $(filter-out $(TARGET),$(MAKECMDGOALS)) + # Hardcode the chainstate dir if we're booting from genesis ifeq ($(TARGET),up-genesis) export CHAINSTATE_DIR := $(PWD)/docker/chainstate/genesis @@ -12,9 +37,6 @@ ifeq ($(TARGET),genesis) export CHAINSTATE_DIR := $(PWD)/docker/chainstate/genesis endif -# UID and GID are not currently used, but may be later to ensure consistent file permissions -export UID := $(shell getent passwd $$(whoami) | cut -d":" -f 3) -export GID := $(shell getent passwd $$(whoami) | cut -d":" -f 4) EPOCH := $(shell date +%s) PWD = $(shell pwd) # Set a unique project name (used for checking if the network is running) @@ -26,22 +48,15 @@ SERVICES := $(shell CHAINSTATE_DIR="" docker compose -f docker/docker-compose.ym # Pauses the bitcoin miner script. Default is set to nearly 1 trillion blocks PAUSE_HEIGHT ?= 999999999999 # Used for the stress testing target. modifies how much cpu to consume for how long -STRESS_CORES ?= $(shell cat /proc/cpuinfo | grep processor | wc -l) STRESS_TIMEOUT ?= 120 # Create the chainstate dir and extract an archive to it when the "up" target is used -$(CHAINSTATE_DIR): /usr/bin/tar /usr/bin/zstd - @if [ ! -d "$(CHAINSTATE_DIR)" ]; then \ - mkdir -p $(CHAINSTATE_DIR) - @if [ "$(TARGET)" = "up" ]; then - if [ -f "$(CHAINSTATE_ARCHIVE)" ]; then - sudo tar --same-owner -xf $(CHAINSTATE_ARCHIVE) -C $(CHAINSTATE_DIR) || exit 1 - else - @echo "Chainstate archive ($(CHAINSTATE_ARCHIVE)) not found. Exiting" - rm -rf $(CHAINSTATE_DIR) - exit 1 - fi - fi +$(CHAINSTATE_DIR): + @if [ ! -d "$(CHAINSTATE_DIR)" ]; then mkdir -p $(CHAINSTATE_DIR) && \ + if [ "$(TARGET)" = "up" ]; then \ + [ -f "$(CHAINSTATE_ARCHIVE)" ] && $(TAR_EXTRACT) $(CHAINSTATE_ARCHIVE) -C $(CHAINSTATE_DIR) || \ + { echo "Chainstate archive ($(CHAINSTATE_ARCHIVE)) not found. Exiting"; rm -rf $(CHAINSTATE_DIR); exit 1; }; \ + fi; \ fi # Build the images with a cache if present @@ -70,33 +85,26 @@ check-not-running: # If the network is not running, we need to exit (ex: trying to restart a container) check-running: - @if test ! `docker compose ls --filter name=$(PROJECT) -q`; then \ - echo "Network not running. exiting"; \ - exit 1; \ - fi + @test `docker compose ls --filter name=$(PROJECT) -q` || { echo "Network not running. exiting"; exit 1; } -# For targets that need an arg, check that *something* is provided. it not, exit +# For targets that need an arg, check that *something* is provided. if not, exit check-params: | check-running - @if [ ! "$(PARAMS)" ]; then \ - echo "No service defined. Exiting"; \ - exit 1; \ - fi + @[ "$(PARAMS)" ] || { echo "No service defined. Exiting"; exit 1; } # Boot the network from a local chainstate archive up: check-not-running | build $(CHAINSTATE_DIR) @echo "Starting $(PROJECT) network from chainstate archive" + @echo " OS: $(OS)" @echo " Chainstate Dir: $(CHAINSTATE_DIR)" @echo " Chainstate Archive: $(CHAINSTATE_ARCHIVE)" echo "$(CHAINSTATE_DIR)" > .current-chainstate-dir docker compose -f docker/docker-compose.yml --profile default -p $(PROJECT) up -d # Boot the network from genesis -genesis: check-not-running | build $(CHAINSTATE_DIR) /usr/bin/sudo +genesis: check-not-running | build $(CHAINSTATE_DIR) @echo "Starting $(PROJECT) network from genesis" - @if [ -d "$(CHAINSTATE_DIR)" ]; then \ - echo " Removing existing genesis chainstate dir: $(CHAINSTATE_DIR)"; \ - sudo rm -rf $(CHAINSTATE_DIR); \ - fi + @echo " OS: $(OS)" + @[ -d "$(CHAINSTATE_DIR)" ] && { echo " Removing existing genesis chainstate dir: $(CHAINSTATE_DIR)"; sudo rm -rf $(CHAINSTATE_DIR); } @echo " Chainstate Dir: $(CHAINSTATE_DIR)" mkdir -p "$(CHAINSTATE_DIR)" echo "$(CHAINSTATE_DIR)" > .current-chainstate-dir @@ -117,9 +125,7 @@ down-prom: down: backup-logs current-chainstate-dir @echo "Shutting down $(PROJECT) network" docker compose -f docker/docker-compose.yml --profile default -p $(PROJECT) down - @if [ -f .current-chainstate-dir ]; then \ - rm -f .current-chainstate-dir - fi + @[ -f .current-chainstate-dir ] && rm -f .current-chainstate-dir # Secondary name to bring down the genesis network down-genesis: down @@ -128,9 +134,7 @@ down-genesis: down down-force: @echo "Force Shutting down $(PROJECT) network" docker compose -f docker/docker-compose.yml --profile default -p $(PROJECT) down - @if [ -f .current-chainstate-dir ]; then \ - rm -f .current-chainstate-dir - fi + @[ -f .current-chainstate-dir ] && rm -f .current-chainstate-dir # Stream specified service logs to STDOUT. Does not validate if PARAMS is supplied log: current-chainstate-dir @@ -142,31 +146,21 @@ log-all: current-chainstate-dir docker compose -f docker/docker-compose.yml --profile default -p $(PROJECT) logs -t -f # Backup all service logs to $ACTIVE_CHAINSTATE_DIR/logs/.log -backup-logs: current-chainstate-dir /usr/bin/sudo +backup-logs: current-chainstate-dir @if [ -f .current-chainstate-dir ]; then \ - $(eval ACTIVE_CHAINSTATE_DIR=$(shell cat .current-chainstate-dir)) - if [ ! -d "$(ACTIVE_CHAINSTATE_DIR)" ]; then \ - echo "Chainstate Dir ($(ACTIVE_CHAINSTATE_DIR)) not found";\ - exit 1; \ - fi; \ - if [ ! -d "$(ACTIVE_CHAINSTATE_DIR)/logs" ]; then \ - mkdir -p $(ACTIVE_CHAINSTATE_DIR)/logs;\ - fi; \ + $(eval ACTIVE_CHAINSTATE_DIR=$(shell cat .current-chainstate-dir)) \ + [ -d "$(ACTIVE_CHAINSTATE_DIR)" ] || { echo "Chainstate Dir ($(ACTIVE_CHAINSTATE_DIR)) not found"; exit 1; }; \ + mkdir -p $(ACTIVE_CHAINSTATE_DIR)/logs; \ echo "Backing up logs to $(ACTIVE_CHAINSTATE_DIR)/logs"; \ - for service in $(SERVICES); do \ - docker logs -t $$service > $(ACTIVE_CHAINSTATE_DIR)/logs/$$service.log 2>&1; \ - done; \ + for service in $(SERVICES); do docker logs -t $$service > $(ACTIVE_CHAINSTATE_DIR)/logs/$$service.log 2>&1; done; \ fi # Replace the existing chainstate archive. Will be used with target `up` snapshot: current-chainstate-dir down @echo "Creating $(PROJECT) chainstate snapshot from $(ACTIVE_CHAINSTATE_DIR)" - @if [ -d "$(ACTIVE_CHAINSTATE_DIR)/logs" ]; then \ - rm -rf $(ACTIVE_CHAINSTATE_DIR)/logs; \ - fi + @[ -d "$(ACTIVE_CHAINSTATE_DIR)/logs" ] && rm -rf $(ACTIVE_CHAINSTATE_DIR)/logs @echo "Creating snapshot: $(CHAINSTATE_ARCHIVE)" - @echo "cd $(ACTIVE_CHAINSTATE_DIR); sudo tar --zstd -cf $(CHAINSTATE_ARCHIVE) *; cd $(PWD)" - cd $(ACTIVE_CHAINSTATE_DIR); sudo tar --zstd -cf $(CHAINSTATE_ARCHIVE) *; cd $(PWD) + (cd $(ACTIVE_CHAINSTATE_DIR) && sudo tar --zstd -cf $(CHAINSTATE_ARCHIVE) *) # Pause all services in the network (netork is down, but recoverably with target 'unpause') pause: @@ -220,5 +214,5 @@ monitor: clean: down-force sudo rm -rf ./docker/chainstate/* -.PHONY: build build-no-cache current-chainstate-dir check-not-running check-running check-params up genesis up-genesis down down-genesis down-force log log-all backup-logs snapshot pause unpause stop start restart stress test monitor clean +.PHONY: build build-no-cache current-chainstate-dir check-not-running check-running check-params up genesis up-genesis down down-genesis down-force log log-all backup-logs snapshot pause unpause stop start restart stress test monitor clean up-prom down-prom .ONESHELL: all-in-one-shell diff --git a/README.md b/README.md index 034fec2..8b8dc93 100644 --- a/README.md +++ b/README.md @@ -1,161 +1,216 @@ -# Hacknet +# Hacknet + - Configured for 3 stacks miners and signers - bind-mounts a local filesystem for data persistence - Uses a chainstate archive to boot the network quickly - Configurable signing weight across the 3 signers +- Designed to run on Linux (tested on Debian-based) or MacOS ## Quickstart ### Start network using a chainstate archive -*Note*: default chainstate archive at `./docker/chainstate.tar.zstd` will be used unless overridden by `CHAINSTATE_ARCHIVE` env var. + +_Note_: default chainstate archive at `./docker/chainstate.tar.zstd` will be used unless overridden by `CHAINSTATE_ARCHIVE` env var. Creates a dynamic chainstate folder at `./docker/chainstate/$(date +%s)` from a chainstate archive + ```sh make up ``` + To override the archive used to restore the network: + ```sh CHAINSTATE_ARCHIVE=./docker/chainsate_new.tar.zstd make up ``` + To override the chainstate dir and resume a stopped network: -*Note*: will not work for the `genesis` chainstate dir and absolute path is required +_Note_: will not work for the `genesis` chainstate dir and absolute path is required + ```sh CHAINSTATE_DIR=$(pwd)/docker/chainsate/ make up ``` ### Start network from genesis + Creates a static chainstate folder at `./docker/chainstate/genesis` + ```sh make genesis ``` ### Stop the network + ```sh make down ``` ## Full list of options + ### Logs + `docker logs -f ` will work, along with some defined Makefile targets #### Store logs from all services under the current chainstate folder + ```sh make backup-logs ``` #### Stream logs from all services + ```sh make log-all ``` + #### Stream single service logs + ```sh make log stacks-signer-1 -- -f ``` #### Log from a single service -*note* this will not follow the logs + +_note_ this will not follow the logs + ```sh make log stacks-signer-1 ``` ### Container management + #### Pause/Unpause service + To pause all services on the network + ```sh make pause ``` + To resume the network + ```sh make unpause ``` #### Restart a service + Used to simulate a node dropping off of the network + ```sh make restart ``` + ex: + ```sh make restart stacks-miner-3 61 ``` #### Stop/Start service (kill) + Stop a single service + ```sh make stop ``` + Restart the stopped service + ```sh make start ``` #### Force stop the hacknet network + If the network is in a "stuck" state where the Makefile targets are not stopping the services (i.e. the `.current-chainstate-dir` file was removed while network was running), `down-force` may be used to force stop the network. ```sh make down-force ``` -Additionally, `clean` target will call `down-force` *and also* delete any chainstates on disk in `./docker/chainstate/*` +Additionally, `clean` target will call `down-force` _and also_ delete any chainstates on disk in `./docker/chainstate/*` + ```sh make clean ``` ### Additional Features + #### Stress the CPU + To simulate CPU load. Can be modified with: + - `STRESS_CORES` to target how many worker threads (default will use all cores) - `STRESS_TIMEOUT` set a timeout (default of 120s) + ```sh make stress ``` + ```sh STRESS_CORES=10 STRESS_TIMEOUT=60 make stress ``` #### Monitor chain heights + Run a script outputting the current chain heights of each miner + ```sh make monitor ``` #### Create a chainstate snapshot + - Setting the env var `PAUSE_HEIGHT` is optional to pause the chain at a specific height, else a default of Bitcoin block `999999999999` is used. - Setting the env var `MINE_INTERVAL_EPOCH3` is recommended to reach the `PAUSE_HEIGHT` more quickly to create the snapshot - Optionally, the `CHAINSTATE_ARCHIVE` env var may be set to store the archive in a non-default location/name -**This operation will work with either the `up` or `genesis` targets** + **This operation will work with either the `up` or `genesis` targets** + ```sh make genesis ``` + or with env vars set: + ```sh MINE_INTERVAL_EPOCH3=10 PAUSE_HEIGHT=240 make genesis ``` + Followed by waiting until the Bitcoin miner reaches the specified height (ex: `docker logs -f bitcoin-miner`) Once the Bitcoin miner has reached the specified height: + ```sh make snapshot ``` + This will first bring down the network, then replace the existing `./docker/chainstate.tar.zstd` archive file used with the `up` Makefile target. -To create the chainstate archive in a non-default location/name *File path must be absolute*: +To create the chainstate archive in a non-default location/name _File path must be absolute_: + ```sh CHAINSTATE_ARCHIVE=$(pwd)/docker/chainstate_new.tar.zstd make snapshot ``` **Note**: `CHAINSTATE_ARCHIVE` must be defined to use with `make up` to use a non-default snapshot. ex: + ```sh CHAINSTATE_ARCHIVE=./docker/chainstate_new.tar.zstd make up ``` #### Prometheus sidecar + ##### Run prometheus and cadvisor + Runs a prometheus container to record data collected by `cadvisor` for tracking host/container metrics + ```sh make up-prom ``` + ##### Stop prometheus and cadvisor + ```sh make down-prom ``` @@ -176,7 +231,8 @@ make down-prom - **tx-broadcaster**: submits token transfer txs to ensure stacks block production during a sortition ## Bitcoin Miner -*Dedicated address for Bitcoin block production after initial setup (~200 blocks). This prevents conflicts with Stacks mining operations.* + +_Dedicated address for Bitcoin block production after initial setup (~200 blocks). This prevents conflicts with Stacks mining operations._ ```text ‣ Mnemonic: foot script pledge suit bread thing stage long auction craft label injury helmet drum ice govern glass tag lamp shield bike raccoon cloud hat @@ -307,13 +363,13 @@ make down-prom ‣ WIF: cMz2ZSsaVgWPFUkE44zHpJepB4NdwB9L938h53hQfFoot81AZFb3 ``` - ## Testing Accounts -*Unused but funded accounts that may be used to deploy contracts or other txs* + +_Unused but funded accounts that may be used to deploy contracts or other txs_ ### Deployer Account -*Unused but funded account that may be used to deploy contracts or other txs* +_Unused but funded account that may be used to deploy contracts or other txs_ ```text ‣ Mnemonic: keep can record bracket note hip face pudding castle detail few sunset review burger enhance foil lamp estate reopen butter then wasp pen kick diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5add6c4..55378cb 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -78,7 +78,7 @@ x-common-vars: - &STACKS_20_HEIGHT ${STACKS_20_HEIGHT:-0} - &STACKS_2_05_HEIGHT ${STACKS_2_05_HEIGHT:-203} - &STACKS_21_HEIGHT ${STACKS_21_HEIGHT:-204} - - &STACKS_POX2_HEIGHT ${STACKS_POX2_HEIGHT:-205} # 104 is is stacks_block=1, 106 is stacks_block=3 + - &STACKS_POX2_HEIGHT ${STACKS_POX2_HEIGHT:-205} # 104 is stacks_block=1, 106 is stacks_block=3 - &STACKS_22_HEIGHT ${STACKS_22_HEIGHT:-206} - &STACKS_23_HEIGHT ${STACKS_23_HEIGHT:-207} - &STACKS_24_HEIGHT ${STACKS_24_HEIGHT:-208} @@ -94,7 +94,7 @@ x-common-vars: - &REWARD_RECIPIENT_1 ${REWARD_RECIPIENT_1:-ST1XVSVQN0KP5SDYFNT8E5TXWVW0XZVQEDBMCJ3XM} # priv: a6143d20cd73d0dce2179e2af7771372a95b9d6795924492bd4d15d17709531e01 - &REWARD_RECIPIENT_2 ${REWARD_RECIPIENT_2:-ST2FW15NGB4H76FMVXKHYYSM865YVS6V3SA1GNABC} # priv: fe3087801196d8027008146b13e6d365920c2e4b7bc9969729ec2f0f22ef74fc01 - &REWARD_RECIPIENT_3 ${REWARD_RECIPIENT_3:-ST2MES40ZEXTX9M4YXW9QSWHRVC9HYT419S198VPM} # priv: ed7eb063c61b8e892987228f1fcfb74eab5009568861613dc4b074b708a7893701 - - &STACKS_CORE_BASE_BRANCH ${STACKS_CORE_BASE_BRANCH:-3.4.0.0.1} + - &STACKS_CORE_BASE_BRANCH ${STACKS_CORE_BASE_BRANCH:-3.4.0.0.2} # branch, tag, or commit SHA - &PAUSE_HEIGHT ${PAUSE_HEIGHT:-999999999999} - &PAUSE_TIMER 86400000 diff --git a/docker/stacks/Dockerfile b/docker/stacks/Dockerfile index 5574bc7..4d867fb 100644 --- a/docker/stacks/Dockerfile +++ b/docker/stacks/Dockerfile @@ -1,14 +1,24 @@ FROM rust:bookworm AS builder # This will be overridden by the value from docker-compose +# Supports any git ref +# Examples: +# STACKS_CORE_BASE_BRANCH=develop (branch) +# STACKS_CORE_BASE_BRANCH=3.3.0.0.1 (tag) +# STACKS_CORE_BASE_BRANCH=abc123def456 (commit) ARG STACKS_CORE_BASE_BRANCH=master -RUN echo "Building Stacks Core from branch: $STACKS_CORE_BASE_BRANCH" +RUN echo "Building Stacks Core from: $STACKS_CORE_BASE_BRANCH" + +# Clone efficiently: shallow for branches/tags, targeted fetch for commits +RUN git init /code/stacks-core && \ + cd /code/stacks-core && \ + git remote add origin https://github.com/stacks-network/stacks-core.git && \ + git fetch --depth=1 origin $STACKS_CORE_BASE_BRANCH && \ + git checkout FETCH_HEAD -# Clone the specified branch from GitHub -RUN git clone --branch $STACKS_CORE_BASE_BRANCH --single-branch --depth=1 https://github.com/stacks-network/stacks-core.git /code/stacks-core WORKDIR /code/stacks-core -RUN apt-get update && apt-get install -y git libclang-dev llvm +RUN apt-get update && apt-get install -y libclang-dev llvm # Run an build that we'll cache the result of and then build the code RUN cargo build --features monitoring_prom,slog_json --bin stacks-node --bin stacks-signer diff --git a/docker/tests/hacknet-liveness.sh b/docker/tests/hacknet-liveness.sh index adfa2fc..3938d20 100755 --- a/docker/tests/hacknet-liveness.sh +++ b/docker/tests/hacknet-liveness.sh @@ -1,220 +1,119 @@ #!/bin/bash - -echo -e " -----------------------------------------------" -echo -e "| => (1) 🔬 TEST: [CHECK BITCOIN NODE IS LIVE] |" -echo -e " -----------------------------------------------" - -CHECK_BTC_LIVENESS_RESULT=$(curl -s -u "hacknet:hacknet" --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "getblockcount", "params": []}' -H 'content-type: text/plain;' "http://localhost:18443/" | jq) - -echo -e "\nGET BLOCKCOUNT RPC:" -echo -e $CHECK_BTC_LIVENESS_RESULT | jq - -BTC_LIVENESS_SUCCESS=$(echo -e $CHECK_BTC_LIVENESS_RESULT | jq -r '.error == null') -BTC_LIVENESS_SUCCESS_FRMT=$([ "$BTC_LIVENESS_SUCCESS" == "true" ] && echo -e "\033[1;32mtrue\033[0m ✅" || echo -e "\033[1;31mfalse\033[0m❌") - - -echo -e "\033[1mBTC_LIVENESS_SUCCESS\033[0m: $BTC_LIVENESS_SUCCESS_FRMT" -echo -e "\n" -######################################################################################## -echo -e " ------------------------------------------------------" -echo -e "| => (2) 🔬 TEST: [CHECK IF BTC MINER IS ABLE TO MINE] |" -echo -e " ------------------------------------------------------" - -echo -e "\nMINE 1 BLOCK RPC:" -MINER_ADDRESS="mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH" -CHECK_IF_BTC_MINEABLE_RESULT=$(curl -s -u "hacknet:hacknet" --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "generatetoaddress", "params": [1, "'$MINER_ADDRESS'"]}' -H 'content-type: text/plain;' "http://localhost:18443/" | jq) - -echo -e $CHECK_IF_BTC_MINEABLE_RESULT | jq - -BTC_MINEABLE_SUCCESS=$(echo -e $CHECK_IF_BTC_MINEABLE_RESULT | jq -r '.error == null') -BTC_MINEABLE_SUCCESS_FRMT=$([ "$BTC_MINEABLE_SUCCESS" == "true" ] && echo -e "\033[1;32mtrue\033[0m ✅" || echo -e "\033[1;31mfalse\033[0m❌") - -echo -e "\033[1mBTC_MINEABLE_SUCCESS\033[0m: $BTC_MINEABLE_SUCCESS_FRMT" -echo -e "\n" -######################################################################################## -echo -e " -----------------------------------------------" -echo -e "| => (3) 🔬 TEST: [CHECK IF POSTGRES IS READY] |" -echo -e " -----------------------------------------------" -PG_READY_SUCCESS=false -PG_READY_SUCCESS_FRMT=$(echo -e "\033[1;31m$PG_READY_SUCCESS\033[0m❌") -if (docker exec -it postgres pg_isready); then - PG_READY_SUCCESS=true - PG_READY_SUCCESS_FRMT=$(echo -e "\033[1;32m$PG_READY_SUCCESS\033[0m ✅") +# Hacknet liveness checks. One line per check. +# Set DEBUG=1 to see context on success too. +set -u + +DEBUG="${DEBUG:-0}" +FAILED=0 +TOTAL=0 + +G='\033[1;32m'; R='\033[1;31m'; D='\033[0;90m'; N='\033[0m' + +# check NAME RC [CONTEXT] +# RC: 0 = pass, non-zero = fail (shell convention) +# CONTEXT: detail printed on fail (or on pass when DEBUG=1) +check() { + local name="$1" rc="$2" ctx="${3-}" + TOTAL=$((TOTAL + 1)) + if [ "$rc" -eq 0 ]; then + echo -e "${G}✓${N} $name" + if [ "$DEBUG" = "1" ] && [ -n "$ctx" ]; then + echo "$ctx" | sed "s/^/ /" + fi + else + FAILED=$((FAILED + 1)) + echo -e "${R}✗${N} $name" + if [ -n "$ctx" ]; then + echo "$ctx" | head -20 | sed "s/^/ /" + fi + fi + return 0 +} + +# 1. Bitcoin RPC live +btc_resp=$(curl -sf -u "hacknet:hacknet" --data-binary '{"jsonrpc":"1.0","method":"getblockcount","params":[]}' -H 'content-type: text/plain;' "http://localhost:18443/" 2>&1 || true) +btc_height=$(echo "$btc_resp" | jq -r '.result // empty' 2>/dev/null) +if [ -n "$btc_height" ]; then + check "bitcoin RPC live (height=$btc_height)" 0 "$btc_resp" +else + check "bitcoin RPC live" 1 "$btc_resp" fi -echo -e "\033[1mPG_READY_SUCCESS\033[0m: $PG_READY_SUCCESS_FRMT" -echo -e "\n" - -NAKAMOTO_SIGNER_DOCKER_LOGS=$(docker logs stacks-signer-1 2>/dev/null) - -NAKAMOTO_SIGNER_READY_SUCCESS=false -NAKAMOTO_SIGNER_READY_SUCCESS_FRMT=$(echo -e "\033[1;31m$NAKAMOTO_SIGNER_READY_SUCCESS\033[0m❌") -if [[ $NAKAMOTO_SIGNER_DOCKER_LOGS == *"Signer spawned successfully"* ]]; then - NAKAMOTO_SIGNER_READY_SUCCESS=true - echo -e "Nakamoto Signer || Signer spawned successfully" - NAKAMOTO_SIGNER_READY_SUCCESS_FRMT=$(echo -e "\033[1;32m$NAKAMOTO_SIGNER_READY_SUCCESS\033[0m ✅") +# 2. Bitcoin mineable +mine_resp=$(curl -sf -u "hacknet:hacknet" --data-binary '{"jsonrpc":"1.0","method":"generatetoaddress","params":[1,"mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH"]}' -H 'content-type: text/plain;' "http://localhost:18443/" 2>&1 || true) +if echo "$mine_resp" | jq -e '.error == null' >/dev/null 2>&1; then + check "bitcoin mineable" 0 "$mine_resp" +else + check "bitcoin mineable" 1 "$mine_resp" fi -echo -e "\033[1mNAKAMOTO_SIGNER_READY_SUCCESS\033[0m: $NAKAMOTO_SIGNER_READY_SUCCESS_FRMT" -echo -e "\n" -############################################################################################################################### -echo -e " --------------------------------------------------" -echo -e "| => (6) 🔬 TEST: [CHECK IF STACKS MINER 1 IS READY] |" -echo -e " --------------------------------------------------" -STX_MINER_1_PORT=20443 -GET_STACKS_MINER_1_INFO_STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null "http://localhost:${STX_MINER_1_PORT}/v2/info") - -echo -e "\nGET STACKS MINER 1 STATUS: $GET_STACKS_MINER_1_INFO_STATUS_CODE" - -STX_MINER_1_LIVENESS_SUCCESS=false -STACKS_MINER_1_LIVENESS_SUCCESS_FRMT=$(echo -e "\033[1;31m$STX_MINER_1_LIVENESS_SUCCESS\033[0m❌") - -if [[ $GET_STACKS_MINER_1_INFO_STATUS_CODE == "200" ]]; then - STX_MINER_1_LIVENESS_SUCCESS=true - STACKS_MINER_1_LIVENESS_SUCCESS_FRMT=$(echo -e "\033[1;32m$STX_MINER_1_LIVENESS_SUCCESS\033[0m ✅") +# 3. Postgres ready +pg_out=$(docker exec postgres pg_isready 2>&1 || true) +if echo "$pg_out" | grep -q "accepting connections"; then + check "postgres ready" 0 "$pg_out" +else + check "postgres ready" 1 "$pg_out" fi - -echo -e "\033[1mSTACKS_MINER_1_LIVENESS_SUCCESS\033[0m: $STACKS_MINER_1_LIVENESS_SUCCESS_FRMT" -echo -e "\n" -############################################################################################################################### -echo -e " --------------------------------------------------" -echo -e "| => (6) 🔬 TEST: [CHECK IF STACKS MINER 2 IS READY] |" -echo -e " --------------------------------------------------" -STX_MINER_2_PORT=21443 -GET_STACKS_MINER_2_INFO_STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null "http://localhost:${STX_MINER_2_PORT}/v2/info") - -echo -e "\nGET STACKS MINER 2 STATUS: $GET_STACKS_MINER_2_INFO_STATUS_CODE" - -STX_MINER_2_LIVENESS_SUCCESS=false -STACKS_MINER_2_LIVENESS_SUCCESS_FRMT=$(echo -e "\033[1;31m$STX_MINER_2_LIVENESS_SUCCESS\033[0m❌") - -if [[ $GET_STACKS_MINER_2_INFO_STATUS_CODE == "200" ]]; then - STX_MINER_2_LIVENESS_SUCCESS=true - STACKS_MINER_2_LIVENESS_SUCCESS_FRMT=$(echo -e "\033[1;32m$STX_MINER_2_LIVENESS_SUCCESS\033[0m ✅") +# 4. Nakamoto signer spawned +signer_logs=$(docker logs stacks-signer-1 2>/dev/null || true) +if echo "$signer_logs" | grep -qF "Signer spawned successfully"; then + check "signer spawned" 0 +else + check "signer spawned" 1 "(no 'Signer spawned successfully' in stacks-signer-1 logs)" fi - -echo -e "\033[1mSTACKS_MINER_2_LIVENESS_SUCCESS\033[0m: $STACKS_MINER_2_LIVENESS_SUCCESS_FRMT" -echo -e "\n" -############################################################################################################################### -echo -e " --------------------------------------------------" -echo -e "| => (6) 🔬 TEST: [CHECK IF STACKS MINER 3 IS READY] |" -echo -e " --------------------------------------------------" -STX_MINER_3_PORT=22443 -GET_STACKS_MINER_3_INFO_STATUS_CODE=$(curl --write-out %{http_code} --silent --output /dev/null "http://localhost:${STX_MINER_3_PORT}/v2/info") - -echo -e "\nGET STACKS MINER 3 STATUS: $GET_STACKS_MINER_3_INFO_STATUS_CODE" - -STX_MINER_3_LIVENESS_SUCCESS=false -STACKS_MINER_3_LIVENESS_SUCCESS_FRMT=$(echo -e "\033[1;31m$STX_MINER_3_LIVENESS_SUCCESS\033[0m❌") - -if [[ $GET_STACKS_MINER_3_INFO_STATUS_CODE == "200" ]]; then - STX_MINER_3_LIVENESS_SUCCESS=true - STACKS_MINER_3_LIVENESS_SUCCESS_FRMT=$(echo -e "\033[1;32m$STX_MINER_3_LIVENESS_SUCCESS\033[0m ✅") +# 5–7. Stacks miners /v2/info +for i in 1 2 3; do + port=$((19443 + i * 1000)) + body=$(curl -sf -m 5 "http://localhost:${port}/v2/info" 2>&1 || true) + if [ -n "$body" ] && echo "$body" | jq -e '.stacks_tip_height' >/dev/null 2>&1; then + h=$(echo "$body" | jq -r '.stacks_tip_height') + check "stacks miner $i live (tip=$h)" 0 "$body" + else + check "stacks miner $i live" 1 "$body" + fi +done + +# 8. Stacks tip > 0 +info=$(curl -sf -m 5 "http://localhost:20443/v2/info" 2>&1 || true) +tip=$(echo "$info" | jq -r '.stacks_tip_height // 0' 2>/dev/null) +tip="${tip:-0}" +if [ "$tip" -gt 0 ] 2>/dev/null; then + check "stacks tip > 0 (tip=$tip)" 0 "$info" +else + check "stacks tip > 0" 1 "$info" fi - -echo -e "\033[1mSTACKS_MINER_2_LIVENESS_SUCCESS\033[0m: $STACKS_MINER_2_LIVENESS_SUCCESS_FRMT" -echo -e "\n" -############################################################################################################################### -echo -e " ---------------------------------------------------------------" -echo -e "| => (7) 🔬 TEST: [CHECK IF STX NODE IS SYNCED WITH BTC UTXOs] |" -echo -e " ---------------------------------------------------------------" - -## (RPC APPROACH) -GET_STACKS_NODE_INFO=$(curl -s "http://localhost:20443/v2/info") - -echo -e "\nGET STACKS NODE INFO:" -echo -e $GET_STACKS_NODE_INFO | jq 'del(.stackerdbs)' -echo -e "\t\t.\n\t\t.\n \033[1;32m<<\033[0m \033[1;35mLong Output Supressed\033[0m \033[1;32m>>\033[0m \n\t\t.\n\t\t." - -STX_SYNC_WITH_BTC_UTXO_SUCCESS=$(echo -e $GET_STACKS_NODE_INFO | jq -r '.stacks_tip_height != 0') -STX_SYNC_WITH_BTC_UTXO_SUCCESS_FRMT=$([ "$STX_SYNC_WITH_BTC_UTXO_SUCCESS" == "true" ] && echo -e "\033[1;32mtrue\033[0m ✅" || echo -e "\033[1;31mfalse\033[0m❌") - -echo -e "\033[1mSTX_SYNC_WITH_BTC_UTXO_SUCCESS\033[0m: $STX_SYNC_WITH_BTC_UTXO_SUCCESS_FRMT" -echo -e "\n" -############################################################################################################################### -echo -e " ---------------------------------------------------------------" -echo -e "| => (8) 🔬 TEST: [CHECK STACKS API EVENT OBSERVER LIVENESS] |" -echo -e " ---------------------------------------------------------------" - -GET_STACKS_API_EVENT_OBSERVER_PING=$(curl -s "http://localhost:3700") - -echo -e "\nGET STACKS API EVENT OBSERVER PING:" -echo -e $GET_STACKS_API_EVENT_OBSERVER_PING | jq - -STACKS_API_EVENT_OBSERVER_LIVENESS_SUCCESS=$(echo -e $GET_STACKS_API_EVENT_OBSERVER_PING | jq -r '.status == "ready"') -STACKS_API_EVENT_OBSERVER_LIVENESS_SUCCESS_FRMT=$([ "$STACKS_API_EVENT_OBSERVER_LIVENESS_SUCCESS" == "true" ] && echo -e "\033[1;32mtrue\033[0m ✅" || echo -e "\033[1;31mfalse\033[0m❌") - -echo -e "\033[1mSTACKS_API_EVENT_OBSERVER_LIVENESS_SUCCESS\033[0m: $STACKS_API_EVENT_OBSERVER_LIVENESS_SUCCESS_FRMT" -echo -e "\n" -############################################################################################################################### -echo -e " ---------------------------------------------------------------" -echo -e "| => (9) 🔬 TEST: [CHECK STACKS PUBLIC API LIVENESS] |" -echo -e " ---------------------------------------------------------------" - -GET_STACKS_PUBLIC_API_PING=$(curl -s --write-out %{http_code} --silent --output /dev/null "http://localhost:3999/extended/") - -echo -e "\nGET STACKS PUBLIC API PING:" -echo -e $GET_STACKS_PUBLIC_API_PING | jq - -STACKS_PUBLIC_API_LIVENESS_SUCCESS=false -STACKS_PUBLIC_API_LIVENESS_SUCCESS_FRMT=$(echo -e "\033[1;31mfalse\033[0m❌") - -if [[ $GET_STACKS_PUBLIC_API_PING == "200" ]]; then - STACKS_PUBLIC_API_LIVENESS_SUCCESS=true - STACKS_PUBLIC_API_LIVENESS_SUCCESS_FRMT=$(echo -e "\033[1;32m$STACKS_PUBLIC_API_LIVENESS_SUCCESS\033[0m ✅") +# 9. Stacks API event observer +evt=$(curl -sf -m 5 "http://localhost:3700" 2>&1 || true) +if echo "$evt" | jq -e '.status == "ready"' >/dev/null 2>&1; then + check "stacks-api event observer ready" 0 "$evt" +else + check "stacks-api event observer ready" 1 "$evt" fi -echo -e "\033[1mSTACKS_PUBLIC_API_LIVENESS_SUCCESS\033[0m: $STACKS_PUBLIC_API_LIVENESS_SUCCESS_FRMT" -echo -e "\n" -############################################################################################################################### -echo -e " -----------------------------------------------------------------" -echo -e "| => (10) 🔬 TEST: [CHECK IF STACKS-API IS CONNECTED TO POSTGRES] |" -echo -e " -----------------------------------------------------------------" - -STACKS_API_DOCKER_LOGS=$(docker logs stacks-api 2>/dev/null) - -STACKS_API_CONNECTED_TO_PG_SUCCESS=false -STACKS_API_CONNECTED_TO_PG_SUCCESS_FRMT=$(echo -e "\033[1;31m$STACKS_API_CONNECTED_TO_PG_SUCCESS\033[0m❌") -if [[ $STACKS_API_DOCKER_LOGS == *"PgNotifier connected"* ]]; then - STACKS_API_CONNECTED_TO_PG_SUCCESS=true - echo -e "Stacks-API || PgNotifier connected" - STACKS_API_CONNECTED_TO_PG_SUCCESS_FRMT=$(echo -e "\033[1;32m$STACKS_API_CONNECTED_TO_PG_SUCCESS\033[0m ✅") +# 10. Stacks public API +if curl -sf -m 5 -o /dev/null "http://localhost:3999/extended/"; then + check "stacks-api public endpoint" 0 +else + check "stacks-api public endpoint" 1 "GET http://localhost:3999/extended/ did not return 2xx" fi -echo -e "\033[1mSTACKS_API_CONNECTED_TO_PG_SUCCESS\033[0m: $STACKS_API_CONNECTED_TO_PG_SUCCESS_FRMT" -echo -e "\n" -############################################################################################################################### -echo -e "-----------------------------------------------------------------" -echo -e "| SUMMARY |" -echo -e "-----------------------------------------------------------------" -echo -e "| \033[1mBTC_LIVENESS_SUCCESS\033[0m: | \t $BTC_LIVENESS_SUCCESS_FRMT |" -echo -e "| \033[1mBTC_MINEABLE_SUCCESS\033[0m: | \t $BTC_MINEABLE_SUCCESS_FRMT |" -echo -e "| \033[1mPG_READY_SUCCESS\033[0m: | \t $PG_READY_SUCCESS_FRMT |" -echo -e "| \033[1mNAKAMOTO_SIGNER_READY_SUCCESS\033[0m: | \t $NAKAMOTO_SIGNER_READY_SUCCESS_FRMT |" -echo -e "| \033[1mSTACKS_MINER_1_LIVENESS_SUCCESS\033[0m: | \t $STACKS_MINER_1_LIVENESS_SUCCESS_FRMT |" -echo -e "| \033[1mSTACKS_MINER_2_LIVENESS_SUCCESS\033[0m: | \t $STACKS_MINER_2_LIVENESS_SUCCESS_FRMT |" -echo -e "| \033[1mSTACKS_MINER_3_LIVENESS_SUCCESS\033[0m: | \t $STACKS_MINER_3_LIVENESS_SUCCESS_FRMT |" -echo -e "| \033[1mSTX_SYNC_WITH_BTC_UTXO_SUCCESS\033[0m: | \t $STX_SYNC_WITH_BTC_UTXO_SUCCESS_FRMT |" -echo -e "| \033[1mSTACKS_API_EVENT_OBSERVER_LIVENESS_SUCCESS\033[0m: | \t $STACKS_API_EVENT_OBSERVER_LIVENESS_SUCCESS_FRMT |" -echo -e "| \033[1mSTACKS_PUBLIC_API_LIVENESS_SUCCESS\033[0m: | \t $STACKS_PUBLIC_API_LIVENESS_SUCCESS_FRMT |" -echo -e "| \033[1mSTACKS_API_CONNECTED_TO_PG_SUCCESS\033[0m: | \t $STACKS_API_CONNECTED_TO_PG_SUCCESS_FRMT |" -echo -e "-----------------------------------------------------------------" +# 11. Stacks-API connected to postgres +api_logs=$(docker logs stacks-api 2>/dev/null || true) +if echo "$api_logs" | grep -qF "PgNotifier connected"; then + check "stacks-api connected to postgres" 0 +else + check "stacks-api connected to postgres" 1 "(no 'PgNotifier connected' in stacks-api logs)" +fi -if [[ $BTC_LIVENESS_SUCCESS == true \ - && $BTC_MINEABLE_SUCCESS == true \ - && $PG_READY_SUCCESS == true \ - && $NAKAMOTO_SIGNER_READY_SUCCESS == true \ - && $STACKS_MINER_1_LIVENESS_SUCCESS == true \ - && $STACKS_MINER_2_LIVENESS_SUCCESS == true \ - && $STACKS_MINER_3_LIVENESS_SUCCESS == true \ - && $STX_SYNC_WITH_BTC_UTXO_SUCCESS == true \ - && $STACKS_API_EVENT_OBSERVER_LIVENESS_SUCCESS == true \ - && $STACKS_PUBLIC_API_LIVENESS_SUCCESS == true \ - && $STACKS_API_CONNECTED_TO_PG_SUCCESS == true ]]; then +# Summary +echo +if [ "$FAILED" -eq 0 ]; then + echo -e "${G}all $TOTAL checks passed${N}" exit 0 +else + echo -e "${R}$FAILED/$TOTAL checks failed${N}" + exit 1 fi - -exit 1