#!/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 '.advanced.zmq-enabled // true') TLS_ENABLED=$(q '.tls.enabled // "disabled"') TLS_PORT=$(q '.tls.port // 3334') # 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 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}" echo "kamado-entrypoint: starting ckpool (solo, ${BITCOIND_VARIANT}) on port ${STRATUM_PORT}" /usr/local/bin/ckpool --btcsolo --config "${CONF}" --sockdir "${SOCKET_DIR}" --log-shares & CKPOOL_PID=$! # 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=$! # 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_PID}" ${STUNNEL_PID:-} 2>/dev/null || true wait "${API_PID}" "${CKPOOL_PID}" ${STUNNEL_PID:-} 2>/dev/null || true exit 0 } trap term TERM INT # shellcheck disable=SC2086 wait -n ${CKPOOL_PID} ${API_PID} ${STUNNEL_PID:-} EXIT_CODE=$? echo "kamado-entrypoint: a supervised process exited (${EXIT_CODE}), stopping the rest" kill -TERM "${API_PID}" "${CKPOOL_PID}" ${STUNNEL_PID:-} 2>/dev/null || true wait || true exit "${EXIT_CODE}"