#!/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}" < 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}"