Expose config options for cleartext stratum port
This commit is contained in:
+390
@@ -0,0 +1,390 @@
|
||||
import { FileHelper } from '@start9labs/start-sdk'
|
||||
import { manifest as bitcoindManifest } from 'bitcoin-core-startos/startos/manifest'
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { storeJson } from './fileModels/store.json'
|
||||
import { i18n } from './i18n'
|
||||
import { sdk } from './sdk'
|
||||
import {
|
||||
bitcoindBridge,
|
||||
btcMountpoint,
|
||||
ckpoolLogDir,
|
||||
ckpoolLogFile,
|
||||
ckpoolRoot,
|
||||
ckpoolSocketDir,
|
||||
curlJson,
|
||||
HealthPayload,
|
||||
kamadoDataDir,
|
||||
kamadoDbPath,
|
||||
kamadoRoot,
|
||||
parseCookie,
|
||||
tlsDir,
|
||||
tlsInternalPort,
|
||||
uiPort,
|
||||
} from './utils'
|
||||
|
||||
const healthUrl = `http://127.0.0.1:${uiPort}/api/health`
|
||||
|
||||
export const main = sdk.setupMain(async ({ effects }) => {
|
||||
/**
|
||||
* ======================== Setup ========================
|
||||
*/
|
||||
console.info('Starting Kamado Pool!')
|
||||
|
||||
// Service settings; reactive, so any config-action change restarts the
|
||||
// daemons with a freshly rendered ckpool.conf.
|
||||
const store = await storeJson.read().const(effects)
|
||||
if (!store) throw new Error('No store.json')
|
||||
|
||||
// bitcoind's RPC + ZMQ endpoints over the LXC bridge (see bitcoindBridge in
|
||||
// utils.ts). Each resolves null while bitcoind is absent; the .const()
|
||||
// watches heal main with a restart when bitcoind appears, disappears, or
|
||||
// changes ports — and never on a routine bitcoind update.
|
||||
const bitcoind = await bitcoindBridge(effects)
|
||||
|
||||
// All Kamado processes (kamado-api, ckpool, stunnel) share ONE
|
||||
// subcontainer, mirroring the single 0.3.x container: kamado-api reaches
|
||||
// ckpool's Unix socket in /run/ckpool and tails its log file without any
|
||||
// cross-container plumbing.
|
||||
const kamadoSub = await sdk.SubContainer.eager(
|
||||
effects,
|
||||
{ imageId: 'main' },
|
||||
sdk.Mounts.of()
|
||||
.mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: kamadoRoot,
|
||||
readonly: false,
|
||||
})
|
||||
.mountVolume({
|
||||
volumeId: 'ckpool',
|
||||
subpath: null,
|
||||
mountpoint: ckpoolRoot,
|
||||
readonly: false,
|
||||
})
|
||||
.mountDependency<typeof bitcoindManifest>({
|
||||
dependencyId: 'bitcoind',
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: btcMountpoint,
|
||||
readonly: true,
|
||||
}),
|
||||
'kamado',
|
||||
)
|
||||
|
||||
// bitcoind uses cookie authentication in 0.4.0 (no more rpcuser/rpcpassword
|
||||
// pointers). Read the cookie from the read-only dependency mount and watch
|
||||
// it: a cookie rotation (bitcoind restart) restarts Kamado with fresh
|
||||
// credentials. Null until bitcoind has started at least once.
|
||||
const cookieRaw = await FileHelper.string(
|
||||
`${kamadoSub.rootfs}/mnt/bitcoind/.cookie`,
|
||||
)
|
||||
.read()
|
||||
.const(effects)
|
||||
const cookie = parseCookie(cookieRaw)
|
||||
|
||||
// Placeholders keep kamado-api bootable while bitcoind is unresolved: the
|
||||
// dashboard comes up, reports Bitcoin Core as unreachable, and the reactive
|
||||
// reads above heal everything once the dependency is satisfied.
|
||||
const rpcAddr = bitcoind.rpc ?? '127.0.0.1:8332'
|
||||
const rpcUser = cookie?.user ?? '__cookie__'
|
||||
const rpcPassword = cookie?.password ?? 'bitcoind-not-yet-available'
|
||||
|
||||
// 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 getbestblockhash every `blockpoll` ms. Only
|
||||
// runs when notify=false — so keep notify=false.
|
||||
// 2. ZMQ hashblock subscriber: instant push from bitcoind. Point it at
|
||||
// the real bridge endpoint; fall back to ckpool's (dead, harmless)
|
||||
// loopback default while bitcoind's ZMQ interface is unavailable.
|
||||
const ckpoolZmqBlock = bitcoind.zmqBlock
|
||||
? `tcp://${bitcoind.zmqBlock}`
|
||||
: 'tcp://127.0.0.1:28332'
|
||||
|
||||
// Rendered ckpool.conf, written to the subcontainer rootfs (ephemeral, so
|
||||
// RPC credentials never touch a persisted volume). `btcaddress` is only
|
||||
// consulted once at startup for ckpool's coinbase-builder self-test; solo
|
||||
// mode pays the worker's stratum address, never this one. The right
|
||||
// self-test address depends on the active network, which ckpool-run.sh
|
||||
// detects from bitcoind at startup and substitutes for the placeholder.
|
||||
const ckpoolConfTemplate = JSON.stringify(
|
||||
{
|
||||
btcd: [
|
||||
{
|
||||
url: rpcAddr,
|
||||
auth: rpcUser,
|
||||
pass: rpcPassword,
|
||||
notify: false,
|
||||
},
|
||||
],
|
||||
btcaddress: '@SELFTEST_ADDRESS@',
|
||||
btcsig: store.coinbaseTag,
|
||||
blockpoll: 100,
|
||||
update_interval: 30,
|
||||
serverurl: [
|
||||
`0.0.0.0:${store.stratumPort}`,
|
||||
`127.0.0.1:${tlsInternalPort}`,
|
||||
],
|
||||
mindiff: store.minDiff,
|
||||
startdiff: store.startDiff,
|
||||
maxdiff: store.maxDiff,
|
||||
dropidle: store.dropIdle,
|
||||
zmqblock: ckpoolZmqBlock,
|
||||
logdir: ckpoolLogDir,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)
|
||||
|
||||
await mkdir(`${kamadoSub.rootfs}/etc/ckpool`, { recursive: true })
|
||||
await writeFile(
|
||||
`${kamadoSub.rootfs}/etc/ckpool/ckpool.conf.template`,
|
||||
ckpoolConfTemplate,
|
||||
)
|
||||
|
||||
// stunnel.conf is rendered here rather than shipped as a static asset,
|
||||
// because the accept port is user config now. `connect` stays on ckpool's
|
||||
// fixed loopback bind so TLS clients keep getting tagged server == 1 (the
|
||||
// dashboard's lock icon).
|
||||
if (store.tlsEnabled) {
|
||||
const stunnelConf = [
|
||||
'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, alerts, cipher rejection) 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:${store.stratumTlsPort}`,
|
||||
`connect = 127.0.0.1:${tlsInternalPort}`,
|
||||
`cert = ${tlsDir}/stratum.pem`,
|
||||
// No client-cert auth — stratum over TLS is opportunistic encryption;
|
||||
// the stratum protocol layer handles miner auth via username.
|
||||
'verify = 0',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
await mkdir(`${kamadoSub.rootfs}/etc/stunnel`, { recursive: true })
|
||||
await writeFile(`${kamadoSub.rootfs}/etc/stunnel/stratum.conf`, stunnelConf)
|
||||
}
|
||||
|
||||
/**
|
||||
* ======================== Daemons ========================
|
||||
*/
|
||||
return sdk.Daemons.of(effects)
|
||||
.addOneshot('dirs', {
|
||||
subcontainer: kamadoSub,
|
||||
exec: {
|
||||
command: [
|
||||
'mkdir',
|
||||
'-p',
|
||||
kamadoDataDir,
|
||||
tlsDir,
|
||||
ckpoolLogDir,
|
||||
ckpoolSocketDir,
|
||||
],
|
||||
},
|
||||
requires: [],
|
||||
})
|
||||
.addDaemon('api', {
|
||||
subcontainer: kamadoSub,
|
||||
exec: {
|
||||
command: ['kamado-api'],
|
||||
env: {
|
||||
LISTEN_ADDR: `:${uiPort}`,
|
||||
CKPOOL_SOCKDIR: ckpoolSocketDir,
|
||||
CKPOOL_LOGFILE: ckpoolLogFile,
|
||||
DB_PATH: kamadoDbPath,
|
||||
BITCOIN_RPC_URL: `http://${rpcAddr}`,
|
||||
BITCOIN_RPC_USER: rpcUser,
|
||||
BITCOIN_RPC_PASSWORD: rpcPassword,
|
||||
POLL_INTERVAL: '5s',
|
||||
KAMADO_LOG_LEVEL: store.logLevel,
|
||||
// Empty disables kamado-api's ZMQ subscriber (RPC polling fallback
|
||||
// remains active either way).
|
||||
BITCOIN_ZMQ_BLOCK:
|
||||
store.zmqEnabled && bitcoind.zmqBlock
|
||||
? `tcp://${bitcoind.zmqBlock}`
|
||||
: '',
|
||||
// Empty means "use mempool.space defaults" for dashboard links.
|
||||
MEMPOOL_BASE_URL: store.mempoolExplorerUrl ?? '',
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
display: i18n('Web Dashboard'),
|
||||
gracePeriod: 15_000,
|
||||
fn: () =>
|
||||
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
||||
successMessage: i18n('The Kamado dashboard is reachable'),
|
||||
errorMessage: i18n('The Kamado dashboard is not reachable'),
|
||||
}),
|
||||
},
|
||||
requires: ['dirs'],
|
||||
})
|
||||
.addDaemon('ckpool', {
|
||||
subcontainer: kamadoSub,
|
||||
exec: {
|
||||
// Waits until bitcoind answers getblockchaininfo, resolves the
|
||||
// network-correct self-test address, renders the final ckpool.conf,
|
||||
// then execs ckpool. When kamado-api kills ckpool on bitcoind
|
||||
// failure (so miners can fail over), StartOS restarts the daemon and
|
||||
// the script blocks again until bitcoind recovers — the 0.3.x
|
||||
// supervised-restart loop, expressed as a daemon.
|
||||
command: ['kamado-ckpool-run.sh'],
|
||||
env: {
|
||||
BITCOIN_RPC_URL: `http://${rpcAddr}`,
|
||||
BITCOIN_RPC_USER: rpcUser,
|
||||
BITCOIN_RPC_PASSWORD: rpcPassword,
|
||||
CKPOOL_SOCKDIR: ckpoolSocketDir,
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
display: i18n('Stratum Server'),
|
||||
gracePeriod: 30_000,
|
||||
fn: () =>
|
||||
sdk.healthCheck.checkPortListening(effects, store.stratumPort, {
|
||||
successMessage: i18n('The stratum server is accepting connections'),
|
||||
errorMessage: i18n(
|
||||
'The stratum server is not accepting connections',
|
||||
),
|
||||
}),
|
||||
},
|
||||
requires: ['dirs'],
|
||||
})
|
||||
.addHealthCheck('bitcoin', {
|
||||
ready: {
|
||||
display: i18n('Bitcoin Core RPC'),
|
||||
fn: async () => {
|
||||
const h = await curlJson<HealthPayload>(kamadoSub, healthUrl)
|
||||
if (!h)
|
||||
return {
|
||||
result: 'failure',
|
||||
message: i18n('Kamado API is unreachable — service may be down'),
|
||||
}
|
||||
if (h.bitcoin)
|
||||
return {
|
||||
result: 'success',
|
||||
message: i18n('Connected to Bitcoin Core'),
|
||||
}
|
||||
return {
|
||||
result: 'failure',
|
||||
message: h.last_error
|
||||
? `${i18n('Bitcoin Core RPC is unreachable')} (${h.last_error})`
|
||||
: i18n('Bitcoin Core RPC is unreachable'),
|
||||
}
|
||||
},
|
||||
},
|
||||
requires: ['api'],
|
||||
})
|
||||
.addHealthCheck('submit-gap', {
|
||||
ready: {
|
||||
display: i18n('Block Submission'),
|
||||
fn: async () => {
|
||||
const h = await curlJson<HealthPayload>(kamadoSub, healthUrl)
|
||||
if (!h)
|
||||
return {
|
||||
result: 'failure',
|
||||
message: i18n('Kamado API is unreachable — service may be down'),
|
||||
}
|
||||
const gap = h.submit_gap ?? 0
|
||||
if (gap === 0)
|
||||
return {
|
||||
result: 'success',
|
||||
message: i18n('All block submissions confirmed'),
|
||||
}
|
||||
return {
|
||||
result: 'failure',
|
||||
message: `${gap} ${i18n(
|
||||
'block(s) submitted to bitcoind but not confirmed — check Bitcoin Core logs',
|
||||
)}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
requires: ['api'],
|
||||
})
|
||||
.addHealthCheck('zmq', () =>
|
||||
store.zmqEnabled
|
||||
? {
|
||||
ready: {
|
||||
display: i18n('ZMQ Block Feed'),
|
||||
fn: async () => {
|
||||
const h = await curlJson<HealthPayload>(kamadoSub, healthUrl)
|
||||
if (!h)
|
||||
return {
|
||||
result: 'failure',
|
||||
message: i18n(
|
||||
'Kamado API is unreachable — service may be down',
|
||||
),
|
||||
}
|
||||
if (h.zmq_stale)
|
||||
return {
|
||||
result: 'failure',
|
||||
message: i18n(
|
||||
'ZMQ block feed is stale — block notifications are falling back to RPC polling',
|
||||
),
|
||||
}
|
||||
return {
|
||||
result: 'success',
|
||||
message: i18n('ZMQ block notifications are flowing'),
|
||||
}
|
||||
},
|
||||
},
|
||||
requires: ['api'],
|
||||
}
|
||||
: null,
|
||||
)
|
||||
.addOneshot('tls-cert', () =>
|
||||
store.tlsEnabled
|
||||
? {
|
||||
subcontainer: kamadoSub,
|
||||
exec: {
|
||||
// Generates (or migrates) the persisted self-signed stratum
|
||||
// certificate under /root/.kamado/tls. Idempotent: regenerates
|
||||
// only when files are missing or the cert-format version marker
|
||||
// is outdated.
|
||||
command: ['kamado-tls-init.sh'],
|
||||
env: { TLS_DIR: tlsDir },
|
||||
},
|
||||
requires: ['dirs'],
|
||||
}
|
||||
: null,
|
||||
)
|
||||
.addDaemon('stunnel', () =>
|
||||
store.tlsEnabled
|
||||
? {
|
||||
subcontainer: kamadoSub,
|
||||
exec: {
|
||||
command: ['stunnel4', '/etc/stunnel/stratum.conf'],
|
||||
},
|
||||
ready: {
|
||||
display: i18n('Stratum TLS'),
|
||||
fn: () =>
|
||||
sdk.healthCheck.checkPortListening(
|
||||
effects,
|
||||
store.stratumTlsPort,
|
||||
{
|
||||
successMessage: i18n(
|
||||
'TLS stratum is accepting connections',
|
||||
),
|
||||
errorMessage: i18n(
|
||||
'TLS stratum is not accepting connections',
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
requires: ['tls-cert'],
|
||||
}
|
||||
: null,
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user