Add regtest smoke test for the block-detection critical path

Validates the end-to-end path log tailer → aggregator → bitcoind
enrichment → /api/blocks without a real miner:

  1. Starts a fresh bitcoind in regtest mode (temp datadir, port 18443)
  2. Mines 102 blocks so the coinbase is mature and the target block
     is live in bitcoind's chain
  3. Starts kamado-api wired to regtest bitcoind (ckpool socket dir
     points at an empty dir — connect failures are expected and
     non-fatal)
  4. Appends "Solved and confirmed block <height>" to the ckpool log
     file; the log tailer picks it up and calls getblockhash to enrich
  5. Polls /api/blocks until the block appears (up to 30 s)
  6. Verifies the hash is a 64-char hex string and matches what bitcoind
     returns for getblockhash at that height

The binary is auto-built from source if go is in PATH; set KAMADO_BIN
to skip the build step. All processes and temp files are cleaned up on
exit via trap.
This commit is contained in:
satoshi
2026-04-28 23:15:58 +03:00
parent d14dded7f0
commit 5b9d4fc55f
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env bash
# Regtest smoke test for kamado-api.
#
# Validates the full critical path:
# ckpool log line → log tailer → aggregator → bitcoind RPC enrichment → /api/blocks
#
# Prerequisites: bitcoind, bitcoin-cli, jq, curl
# The kamado-api binary is auto-built from source if go is in PATH;
# alternatively set KAMADO_BIN to point at a pre-built binary.
#
# Usage:
# test/regtest_smoke.sh
# KAMADO_BIN=./api/bin/kamado-api test/regtest_smoke.sh
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
API_DIR="$SCRIPT_DIR/../api"
# ---- helpers ----------------------------------------------------------------
log() { printf '[smoke] %s\n' "$*"; }
die() { printf '[smoke] FAIL: %s\n' "$*" >&2; exit 1; }
pass() { printf '[smoke] PASS: %s\n' "$*"; exit 0; }
WORK_DIR=""
BITCOIN_PID=""
KAMADO_PID=""
cleanup() {
[[ -n "$KAMADO_PID" ]] && kill "$KAMADO_PID" 2>/dev/null || true
[[ -n "$BITCOIN_PID" ]] && kill "$BITCOIN_PID" 2>/dev/null || true
[[ -n "$WORK_DIR" ]] && rm -rf "$WORK_DIR"
}
trap cleanup EXIT
# ---- prerequisites ----------------------------------------------------------
for cmd in bitcoind bitcoin-cli jq curl; do
command -v "$cmd" >/dev/null 2>&1 || die "required command not found: $cmd"
done
# Locate or build kamado-api.
if [[ -n "${KAMADO_BIN:-}" ]]; then
[[ -x "$KAMADO_BIN" ]] || die "KAMADO_BIN=$KAMADO_BIN is not executable"
elif command -v go >/dev/null 2>&1; then
WORK_DIR=$(mktemp -d)
log "building kamado-api…"
(cd "$API_DIR" && go build -o "$WORK_DIR/kamado-api" ./cmd/kamado-api) \
|| die "go build failed"
KAMADO_BIN="$WORK_DIR/kamado-api"
else
die "KAMADO_BIN not set and go not in PATH; build the binary first:
cd api && go build -o bin/kamado-api ./cmd/kamado-api
KAMADO_BIN=api/bin/kamado-api test/regtest_smoke.sh"
fi
[[ -n "${WORK_DIR:-}" ]] || WORK_DIR=$(mktemp -d)
# ---- configuration ----------------------------------------------------------
BTC_DIR="$WORK_DIR/bitcoin"
BTC_RPC_PORT=18443 # regtest default
BTC_USER=smoketest
BTC_PASS=smokepass
KAMADO_PORT=19080 # avoid clashing with a running instance
CKPOOL_LOG="$WORK_DIR/ckpool.log"
KAMADO_DB="$WORK_DIR/kamado.db"
SOCK_DIR="$WORK_DIR/sockets" # no real ckpool; connect attempts fail gracefully
mkdir -p "$BTC_DIR" "$SOCK_DIR"
touch "$CKPOOL_LOG"
# Shorthand for bitcoin-cli pointing at the regtest instance.
cli() {
bitcoin-cli \
-regtest \
-datadir="$BTC_DIR" \
-rpcport="$BTC_RPC_PORT" \
-rpcuser="$BTC_USER" \
-rpcpassword="$BTC_PASS" \
"$@"
}
# ---- start bitcoind ---------------------------------------------------------
log "starting bitcoind regtest on port $BTC_RPC_PORT"
bitcoind \
-regtest \
-datadir="$BTC_DIR" \
-rpcport="$BTC_RPC_PORT" \
-rpcuser="$BTC_USER" \
-rpcpassword="$BTC_PASS" \
-rpcbind=127.0.0.1 \
-rpcallowip=127.0.0.1 \
-fallbackfee=0.0001 \
-nodaemon \
>"$WORK_DIR/bitcoind.log" 2>&1 &
BITCOIN_PID=$!
log "waiting for bitcoind RPC…"
ready=false
for i in $(seq 1 30); do
if cli getblockcount >/dev/null 2>&1; then ready=true; break; fi
sleep 1
done
$ready || die "bitcoind did not become ready within 30s (see $WORK_DIR/bitcoind.log)"
log "bitcoind ready (pid $BITCOIN_PID)"
# Create a descriptor wallet and mine enough blocks for coinbase maturity,
# then mine the one block we'll claim to have "solved".
log "setting up wallet and generating blocks"
cli createwallet smoke >/dev/null 2>&1 || cli loadwallet smoke >/dev/null 2>&1 || true
MINE_ADDR=$(cli getnewaddress)
# 101 maturity blocks so the wallet's coinbase is spendable, then the
# target block that we'll inject into the ckpool log.
cli generatetoaddress 101 "$MINE_ADDR" >/dev/null
BEFORE_HEIGHT=$(cli getblockcount)
cli generatetoaddress 1 "$MINE_ADDR" >/dev/null
TARGET_HEIGHT=$((BEFORE_HEIGHT + 1))
log "target block at height $TARGET_HEIGHT is live in bitcoind"
# ---- start kamado-api -------------------------------------------------------
log "starting kamado-api on port $KAMADO_PORT"
LISTEN_ADDR=":$KAMADO_PORT" \
BITCOIN_RPC_URL="http://127.0.0.1:$BTC_RPC_PORT" \
BITCOIN_RPC_USER="$BTC_USER" \
BITCOIN_RPC_PASSWORD="$BTC_PASS" \
BITCOIN_ZMQ_BLOCK="" \
CKPOOL_SOCKDIR="$SOCK_DIR" \
CKPOOL_LOGFILE="$CKPOOL_LOG" \
DB_PATH="$KAMADO_DB" \
POLL_INTERVAL=2s \
"$KAMADO_BIN" >"$WORK_DIR/kamado.log" 2>&1 &
KAMADO_PID=$!
log "waiting for kamado-api snapshot endpoint…"
ready=false
for i in $(seq 1 30); do
if curl -sf "http://127.0.0.1:$KAMADO_PORT/api/snapshot" >/dev/null 2>&1; then
ready=true; break
fi
# Bail early if the process died.
kill -0 "$KAMADO_PID" 2>/dev/null || die "kamado-api exited unexpectedly (see $WORK_DIR/kamado.log)"
sleep 1
done
$ready || die "kamado-api did not become ready within 30s (see $WORK_DIR/kamado.log)"
log "kamado-api ready (pid $KAMADO_PID)"
# ---- inject the ckpool solve line -------------------------------------------
#
# The block is already in bitcoind (generated above), so the tailer's
# enrichment call (getblockhash → getblock) will succeed immediately.
log "writing 'Solved and confirmed block $TARGET_HEIGHT' to ckpool log"
printf 'Solved and confirmed block %d\n' "$TARGET_HEIGHT" >>"$CKPOOL_LOG"
# ---- wait for the block to appear in /api/blocks ----------------------------
log "polling /api/blocks for height $TARGET_HEIGHT (up to 30s)…"
found=false
for i in $(seq 1 30); do
RESP=$(curl -sf "http://127.0.0.1:$KAMADO_PORT/api/blocks" 2>/dev/null || echo "[]")
if echo "$RESP" | jq -e "any(.[]; .height == $TARGET_HEIGHT)" >/dev/null 2>&1; then
found=true; break
fi
sleep 1
done
if ! $found; then
log "last /api/blocks response: $(curl -sf "http://127.0.0.1:$KAMADO_PORT/api/blocks" || echo "(unreachable)")"
log "kamado-api log tail:"
tail -20 "$WORK_DIR/kamado.log" | sed 's/^/ /'
die "block $TARGET_HEIGHT did not appear in /api/blocks within 30s"
fi
# ---- verify enrichment: block must carry a hash from bitcoind ---------------
BLOCK_HASH=$(
curl -sf "http://127.0.0.1:$KAMADO_PORT/api/blocks" \
| jq -r "first(.[] | select(.height == $TARGET_HEIGHT) | .hash // \"\")"
)
if [[ -z "$BLOCK_HASH" || "$BLOCK_HASH" == "null" ]]; then
die "block $TARGET_HEIGHT in /api/blocks but hash enrichment failed (hash=$BLOCK_HASH)"
fi
# Verify the hash looks like a real block hash (64 hex chars).
if [[ ! "$BLOCK_HASH" =~ ^[0-9a-f]{64}$ ]]; then
die "block hash looks wrong: $BLOCK_HASH"
fi
# Cross-check: the hash must match what bitcoind says is at that height.
CANONICAL=$(cli getblockhash "$TARGET_HEIGHT")
if [[ "$BLOCK_HASH" != "$CANONICAL" ]]; then
die "hash mismatch: kamado has $BLOCK_HASH, bitcoind says $CANONICAL"
fi
pass "block $TARGET_HEIGHT → hash $BLOCK_HASH (matches bitcoind canonical)"