ZMQ block notifications are important enough to be visible without expanding Advanced. Advanced fields no longer show a required asterisk since they all have sane defaults and the entrypoint already falls back to them via yq.
357 lines
14 KiB
Bash
Executable File
357 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
# Kamado Pool StartOS entrypoint.
|
|
#
|
|
# Reads /root/.kamado/start9/config.yaml via yq, resolves the chosen
|
|
# bitcoind variant (mainnet vs testnet4), exports the env vars the
|
|
# upstream ckpool entrypoint expects, renders ckpool.conf from the
|
|
# template shipped alongside this script, and then supervises
|
|
# ckpool-solo + kamado-api as a pair.
|
|
set -euo pipefail
|
|
|
|
CONFIG_FILE="/root/.kamado/start9/config.yaml"
|
|
if [[ ! -f "${CONFIG_FILE}" ]]; then
|
|
echo "kamado-entrypoint: config file missing: ${CONFIG_FILE}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
q() { yq -r "$1" "${CONFIG_FILE}"; }
|
|
|
|
BITCOIND_VARIANT=$(q '.bitcoind.type')
|
|
case "${BITCOIND_VARIANT}" in
|
|
bitcoind)
|
|
export BITCOIN_RPC_HOST="bitcoind.embassy"
|
|
export BITCOIN_RPC_PORT=8332
|
|
# Satoshi's genesis block coinbase address. Used only for
|
|
# ckpool's startup coinbase-builder self-test; never credited
|
|
# a satoshi since solo mode pays the worker's stratum address.
|
|
SELFTEST_ADDRESS="1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
|
|
;;
|
|
bitcoind-testnet)
|
|
export BITCOIN_RPC_HOST="bitcoind-testnet.embassy"
|
|
export BITCOIN_RPC_PORT=48332
|
|
# Testnet genesis coinbase address — valid P2PKH on testnet4.
|
|
SELFTEST_ADDRESS="mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn"
|
|
;;
|
|
*)
|
|
echo "kamado-entrypoint: unknown bitcoind variant: ${BITCOIND_VARIANT}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
export BITCOIN_RPC_USER=$(q '.bitcoind.user')
|
|
export BITCOIN_RPC_PASSWORD=$(q '.bitcoind.password')
|
|
export STRATUM_PORT=$(q '.stratum-port // 3333')
|
|
export POOL_BTCSIG=$(q '.advanced.pool-identifier // "/Kamado/"')
|
|
export STARTDIFF=$(q '.advanced.startdiff // 16384')
|
|
export MINDIFF=$(q '.advanced.mindiff // 1000')
|
|
export MAXDIFF=$(q '.advanced.maxdiff // 0')
|
|
export DROPIDLE=$(q '.advanced.dropidle // 0')
|
|
LOG_LEVEL=$(q '.advanced.log-level // "info"')
|
|
ZMQ_ENABLED=$(q '.zmq-enabled // true')
|
|
TLS_ENABLED=$(q '.tls.enabled // "disabled"')
|
|
TLS_PORT=$(q '.tls.port // 3334')
|
|
|
|
# Empty MEMPOOL_BASE_URL means "use mempool.space defaults". When the
|
|
# user picks "Custom URL" in advanced config, we surface the value so
|
|
# kamado-api can include it in the snapshot and the UI can rewrite
|
|
# explorer links to point at the user's own mempool instance.
|
|
MEMPOOL_TYPE=$(q '.advanced.mempool-explorer.type // "default"')
|
|
if [[ "${MEMPOOL_TYPE}" == "custom" ]]; then
|
|
MEMPOOL_BASE_URL=$(q '.advanced.mempool-explorer.url // ""')
|
|
else
|
|
MEMPOOL_BASE_URL=""
|
|
fi
|
|
export MEMPOOL_BASE_URL
|
|
|
|
# CKPool-solo uses the worker's stratum username as the payout
|
|
# address and refuses to authenticate workers whose username is not
|
|
# a valid address on the active network. The conf `btcaddress` is
|
|
# only consulted once at startup, for ckpool's coinbase-builder
|
|
# self-test: it builds and validates a sample coinbase transaction
|
|
# against bitcoind before accepting any workers. Since no worker has
|
|
# connected yet at that point, we hand it the genesis block coinbase
|
|
# address for the active network — it's always valid, and solo mode
|
|
# never credits it a satoshi.
|
|
export POOL_BTCADDRESS="${SELFTEST_ADDRESS}"
|
|
|
|
# LOGDIR lives on the ckpool data volume so ckpool's own state files
|
|
# (users/, workers/, pool/pool.status, daily log files) survive restart.
|
|
# SOCKET_DIR is ephemeral — sockets are re-created on each start.
|
|
export LOGDIR=/root/.ckpool/logs
|
|
export SOCKET_DIR=/run/ckpool
|
|
|
|
# ckpool has TWO independent new-block detection paths. Wire up both
|
|
# so we're never blind to a tip change (every second of stale work
|
|
# in solo mode is hashrate burned on a dead block).
|
|
#
|
|
# 1. Blockpoll thread: polls bitcoind's getbestblockhash every
|
|
# BLOCKPOLL_MS. This only runs when notify=false — with
|
|
# notify=true the thread sleeps 5s and returns immediately.
|
|
# So keep notify=false.
|
|
# 2. ZMQ hashblock subscriber: instant push notifications from
|
|
# bitcoind. ckpool defaults zmqblock to tcp://127.0.0.1:28332
|
|
# which is useless in this container, so point it at the real
|
|
# endpoint explicitly.
|
|
export BITCOIN_NOTIFY=false
|
|
export ZMQ_BLOCK="tcp://${BITCOIN_RPC_HOST}:28332"
|
|
export BLOCKPOLL_MS=100
|
|
export UPDATE_INTERVAL_S=30
|
|
|
|
# ckpool binds two stratum sockets — a public plaintext port and a
|
|
# loopback-only port that stunnel forwards TLS traffic to. Clients
|
|
# arriving via the second bind get client.server == 1 in the runtime
|
|
# JSON, which lets the dashboard tag them with a TLS lock icon
|
|
# without resorting to source-IP heuristics. Even when TLS is
|
|
# disabled the second bind is harmless (nothing connects to it).
|
|
export TLS_INTERNAL_PORT=3437
|
|
|
|
# CKPool loglevel: 6 = LOG_INFO, required for share-level logging
|
|
# (Accepted/Rejected client lines) used by the stats feature.
|
|
export CKPOOL_LOGLEVEL=6
|
|
|
|
mkdir -p "${LOGDIR}" "${SOCKET_DIR}" /etc/ckpool
|
|
|
|
# Render ckpool.conf using the same sed approach as the upstream
|
|
# KamadoPool ckpool entrypoint — the template is bundled into the
|
|
# image at build time.
|
|
TEMPLATE=/etc/ckpool/ckpool.conf.template
|
|
CONF=/etc/ckpool/ckpool.conf
|
|
sed \
|
|
-e "s|\${BITCOIN_RPC_HOST}|${BITCOIN_RPC_HOST}|g" \
|
|
-e "s|\${BITCOIN_RPC_PORT}|${BITCOIN_RPC_PORT}|g" \
|
|
-e "s|\${BITCOIN_RPC_USER}|${BITCOIN_RPC_USER}|g" \
|
|
-e "s|\${BITCOIN_RPC_PASSWORD}|${BITCOIN_RPC_PASSWORD}|g" \
|
|
-e "s|\${BITCOIN_NOTIFY}|${BITCOIN_NOTIFY}|g" \
|
|
-e "s|\${POOL_BTCADDRESS}|${POOL_BTCADDRESS}|g" \
|
|
-e "s|\${POOL_BTCSIG}|${POOL_BTCSIG}|g" \
|
|
-e "s|\${BLOCKPOLL_MS}|${BLOCKPOLL_MS}|g" \
|
|
-e "s|\${UPDATE_INTERVAL_S}|${UPDATE_INTERVAL_S}|g" \
|
|
-e "s|\${STRATUM_PORT}|${STRATUM_PORT}|g" \
|
|
-e "s|\${TLS_INTERNAL_PORT}|${TLS_INTERNAL_PORT}|g" \
|
|
-e "s|\${MINDIFF}|${MINDIFF}|g" \
|
|
-e "s|\${STARTDIFF}|${STARTDIFF}|g" \
|
|
-e "s|\${MAXDIFF}|${MAXDIFF}|g" \
|
|
-e "s|\${DROPIDLE}|${DROPIDLE}|g" \
|
|
-e "s|\${LOGDIR}|${LOGDIR}|g" \
|
|
-e "s|\${ZMQ_BLOCK}|${ZMQ_BLOCK}|g" \
|
|
"${TEMPLATE}" > "${CONF}"
|
|
|
|
# DB_PATH must live on the persisted main volume; the default
|
|
# /var/lib/kamado/kamado.db is ephemeral container storage.
|
|
KAMADO_DATA_DIR=/root/.kamado/data
|
|
mkdir -p "${KAMADO_DATA_DIR}"
|
|
export LISTEN_ADDR=":8080"
|
|
export CKPOOL_SOCKDIR="${SOCKET_DIR}"
|
|
export CKPOOL_LOGFILE="${LOGDIR}/ckpool.log"
|
|
export DB_PATH="${KAMADO_DATA_DIR}/kamado.db"
|
|
export BITCOIN_RPC_URL="http://${BITCOIN_RPC_HOST}:${BITCOIN_RPC_PORT}"
|
|
export POLL_INTERVAL=5s
|
|
export KAMADO_LOG_LEVEL="${LOG_LEVEL}"
|
|
if [[ "${ZMQ_ENABLED}" == "true" ]]; then
|
|
export BITCOIN_ZMQ_BLOCK="tcp://${BITCOIN_RPC_HOST}:28332"
|
|
else
|
|
export BITCOIN_ZMQ_BLOCK=""
|
|
fi
|
|
|
|
echo "kamado-entrypoint: starting kamado-api"
|
|
/usr/local/bin/kamado-api &
|
|
API_PID=$!
|
|
|
|
# wait_for_bitcoind blocks until bitcoind responds to getblockchaininfo.
|
|
# Called before each ckpool start so we don't launch ckpool into a wall.
|
|
wait_for_bitcoind() {
|
|
local url="http://${BITCOIN_RPC_USER}:${BITCOIN_RPC_PASSWORD}@${BITCOIN_RPC_HOST}:${BITCOIN_RPC_PORT}"
|
|
local backoff=2
|
|
while true; do
|
|
if curl -sf --max-time 5 \
|
|
-d '{"jsonrpc":"1.0","method":"getblockchaininfo","params":[]}' \
|
|
-H 'Content-Type: application/json' \
|
|
"${url}" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
echo "kamado-entrypoint: waiting for bitcoind (retry in ${backoff}s)..."
|
|
sleep "${backoff}"
|
|
backoff=$(( backoff < 30 ? backoff * 2 : 30 ))
|
|
done
|
|
}
|
|
|
|
# Supervised ckpool restart loop. When ckpool exits (killed by the API
|
|
# on bitcoind failure, or crashed), we wait for bitcoind to be reachable
|
|
# again before restarting. This keeps ckpool alive when bitcoind is
|
|
# healthy and lets miners failover when it's not — without restart-
|
|
# looping the entire container.
|
|
run_ckpool_loop() {
|
|
while true; do
|
|
wait_for_bitcoind
|
|
echo "kamado-entrypoint: starting ckpool (solo, ${BITCOIND_VARIANT}) on port ${STRATUM_PORT}, loglevel ${CKPOOL_LOGLEVEL}"
|
|
/usr/local/bin/ckpool --btcsolo --config "${CONF}" --sockdir "${SOCKET_DIR}" --log-shares -l "${CKPOOL_LOGLEVEL}"
|
|
EXIT_CODE=$?
|
|
echo "kamado-entrypoint: ckpool exited (code ${EXIT_CODE}), will restart after bitcoind is reachable"
|
|
sleep 2
|
|
done
|
|
}
|
|
run_ckpool_loop &
|
|
CKPOOL_LOOP_PID=$!
|
|
|
|
# Optional TLS stratum via stunnel sidecar.
|
|
STUNNEL_PID=""
|
|
if [[ "${TLS_ENABLED}" == "enabled" ]]; then
|
|
TLS_DIR=/root/.kamado/tls
|
|
CRT="${TLS_DIR}/stratum.crt"
|
|
KEY="${TLS_DIR}/stratum.key"
|
|
CERT="${TLS_DIR}/stratum.pem"
|
|
MARKER="${TLS_DIR}/cert_version"
|
|
mkdir -p "${TLS_DIR}"
|
|
|
|
# Bump TLS_CERT_VERSION any time the cert format/extensions change.
|
|
# The startup check regenerates whenever the marker file is missing
|
|
# or doesn't match this version. This is more reliable than poking
|
|
# at the existing cert's extensions — we know *exactly* when a new
|
|
# shape is required and the upgrade self-heals on next boot.
|
|
# v4: broaden subjectAltName to cover mDNS / LAN / Tor hostnames
|
|
# so miner firmwares that verify the SAN against the hostname
|
|
# they were pointed at (e.g. AxeOS connecting to host.local)
|
|
# stop failing with MBEDTLS_ERR_X509_CERT_VERIFY_FAILED.
|
|
TLS_CERT_VERSION=4
|
|
|
|
NEEDS_REGEN=false
|
|
if [[ ! -f "${CERT}" || ! -f "${CRT}" || ! -f "${KEY}" ]]; then
|
|
NEEDS_REGEN=true
|
|
elif [[ ! -f "${MARKER}" ]] \
|
|
|| [[ "$(cat "${MARKER}" 2>/dev/null)" != "${TLS_CERT_VERSION}" ]]; then
|
|
echo "kamado-entrypoint: TLS cert is older format (want v${TLS_CERT_VERSION}); regenerating"
|
|
NEEDS_REGEN=true
|
|
fi
|
|
|
|
if [[ "${NEEDS_REGEN}" == "true" ]]; then
|
|
echo "kamado-entrypoint: generating self-signed stratum TLS cert v${TLS_CERT_VERSION}"
|
|
# Write the extensions to a config file rather than rely on
|
|
# `-addext`: some openssl builds emit them into unpredictable
|
|
# locations (e.g. CSR instead of the cert), and this is the
|
|
# documented, cross-version way to pin the full extension set.
|
|
CONF=$(mktemp)
|
|
cat > "${CONF}" <<'OPENSSL_CONF'
|
|
[ req ]
|
|
default_bits = 2048
|
|
default_md = sha256
|
|
prompt = no
|
|
distinguished_name = req_dn
|
|
x509_extensions = v3_cert
|
|
|
|
[ req_dn ]
|
|
CN = kamado-pool
|
|
|
|
[ v3_cert ]
|
|
basicConstraints = critical, CA:FALSE
|
|
keyUsage = critical, digitalSignature, keyEncipherment
|
|
extendedKeyUsage = serverAuth
|
|
subjectKeyIdentifier = hash
|
|
subjectAltName = @alt_names
|
|
|
|
[ alt_names ]
|
|
# Specific StartOS / local names the pool might be reached through.
|
|
DNS.1 = kamado-pool.embassy
|
|
DNS.2 = kamado-pool
|
|
DNS.3 = localhost
|
|
# Wildcard SANs covering the TLDs miners typically use:
|
|
# *.local -> mDNS / Bonjour (e.g. obese-admirer.local on AxeOS)
|
|
# *.embassy -> StartOS inter-service hostnames
|
|
# *.onion -> Tor hidden services
|
|
# *.home.arpa -> RFC 8375 home network namespace
|
|
# *.lan -> common consumer router default TLD
|
|
# *.internal -> some LAN setups
|
|
# Strictly leftmost-label wildcards per RFC 6125; libraries that
|
|
# enforce this (mbedtls, OpenSSL, BoringSSL, Go crypto/tls) all
|
|
# accept them.
|
|
DNS.4 = *.local
|
|
DNS.5 = *.embassy
|
|
DNS.6 = *.onion
|
|
DNS.7 = *.home.arpa
|
|
DNS.8 = *.lan
|
|
DNS.9 = *.internal
|
|
IP.1 = 127.0.0.1
|
|
OPENSSL_CONF
|
|
|
|
openssl req -x509 -newkey rsa:2048 -nodes \
|
|
-keyout "${KEY}" \
|
|
-out "${CRT}" \
|
|
-days 3650 \
|
|
-config "${CONF}" \
|
|
>/dev/null 2>&1
|
|
rm -f "${CONF}"
|
|
|
|
# stunnel reads cert+key in either order, but cert-first is the
|
|
# convention openssl and most tooling expect.
|
|
cat "${CRT}" "${KEY}" > "${CERT}"
|
|
chmod 600 "${KEY}" "${CERT}"
|
|
printf '%s\n' "${TLS_CERT_VERSION}" > "${MARKER}"
|
|
|
|
# Log the extensions so operators can verify the cert is sane
|
|
# from the service logs without needing to exec into the
|
|
# container.
|
|
echo "kamado-entrypoint: cert extensions:"
|
|
openssl x509 -in "${CRT}" -noout -ext subjectAltName,extendedKeyUsage,keyUsage 2>&1 \
|
|
| sed 's/^/ /'
|
|
fi
|
|
|
|
FINGERPRINT=$(openssl x509 -in "${CRT}" -noout -fingerprint -sha256 | cut -d= -f2)
|
|
printf '%s\n' "${FINGERPRINT}" > "${TLS_DIR}/fingerprint.txt"
|
|
echo "kamado-entrypoint: stratum TLS SHA256 fingerprint: ${FINGERPRINT}"
|
|
|
|
STUNNEL_CONF=/etc/stunnel/stratum.conf
|
|
mkdir -p /etc/stunnel
|
|
cat > "${STUNNEL_CONF}" <<EOF
|
|
foreground = yes
|
|
pid =
|
|
output = /dev/stdout
|
|
# debug = 5 (notice) so each successful TLS handshake produces a
|
|
# "Service [stratum] accepted connection" / "connected from" pair
|
|
# in the service logs. Failures (bad cert, alert messages, cipher
|
|
# rejection) still surface at level 3, so both happy- and sad-path
|
|
# events are visible without flipping levels per incident.
|
|
debug = 5
|
|
# Pin a modern TLS floor. Any miner firmware younger than ~2018
|
|
# speaks TLS 1.2, and TLS 1.0/1.1 are deprecated anyway.
|
|
sslVersion = all
|
|
options = NO_SSLv2
|
|
options = NO_SSLv3
|
|
options = NO_TLSv1
|
|
options = NO_TLSv1_1
|
|
|
|
[stratum]
|
|
accept = 0.0.0.0:${TLS_PORT}
|
|
# Forward to ckpool's loopback-only TLS bind, not the public plaintext
|
|
# port. ckpool tags traffic by serverurl index (server == 1 -> TLS),
|
|
# which the dashboard reads to render a lock icon next to the client.
|
|
connect = 127.0.0.1:${TLS_INTERNAL_PORT}
|
|
cert = ${CERT}
|
|
# No client-cert auth — stratum over TLS is opportunistic encryption;
|
|
# the stratum protocol layer handles miner auth via username.
|
|
verify = 0
|
|
EOF
|
|
|
|
echo "kamado-entrypoint: starting stunnel on :${TLS_PORT} -> :${STRATUM_PORT}"
|
|
/usr/bin/stunnel4 "${STUNNEL_CONF}" &
|
|
STUNNEL_PID=$!
|
|
fi
|
|
|
|
term() {
|
|
echo "kamado-entrypoint: SIGTERM — shutting down"
|
|
kill -TERM "${API_PID}" "${CKPOOL_LOOP_PID}" ${STUNNEL_PID:-} 2>/dev/null || true
|
|
# Kill any running ckpool process inside the loop.
|
|
pkill -TERM -f '/usr/local/bin/ckpool' 2>/dev/null || true
|
|
wait "${API_PID}" "${CKPOOL_LOOP_PID}" ${STUNNEL_PID:-} 2>/dev/null || true
|
|
exit 0
|
|
}
|
|
trap term TERM INT
|
|
|
|
# The API is the critical process — if it exits, the container should
|
|
# restart. The ckpool loop manages its own lifecycle independently.
|
|
wait "${API_PID}"
|
|
EXIT_CODE=$?
|
|
echo "kamado-entrypoint: kamado-api exited (${EXIT_CODE}), stopping the rest"
|
|
kill -TERM "${CKPOOL_LOOP_PID}" ${STUNNEL_PID:-} 2>/dev/null || true
|
|
pkill -TERM -f '/usr/local/bin/ckpool' 2>/dev/null || true
|
|
wait || true
|
|
exit "${EXIT_CODE}"
|