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:
Executable
+201
@@ -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)"
|
||||||
Reference in New Issue
Block a user