Bump version to 0.2.3 with new upstream features and bug fixes
This commit is contained in:
5 files changed
+216
-13
No files matched your search
+160
-10
@@ -2,6 +2,8 @@ import { storeJson } from '../fileModels/store.json'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import {
|
||||
ckpoolLogFile,
|
||||
ckpoolRoot,
|
||||
curlJson,
|
||||
defaultStratumPort,
|
||||
defaultStratumPublicTlsPort,
|
||||
@@ -112,6 +114,17 @@ interface Snapshot {
|
||||
hashrate_hs_1h: number
|
||||
hashrate_hs_24h: number
|
||||
best_diff: number
|
||||
best_share_hash?: string
|
||||
best_share_pow?: {
|
||||
sdiff: number
|
||||
height: number
|
||||
hash: string
|
||||
seen_at: string
|
||||
}
|
||||
// Log-derived (tailer) session stats — nonzero proves kamado-api is
|
||||
// actually reading ckpool's log, as opposed to the socket-derived
|
||||
// share counters above.
|
||||
diff_dist_session: number[]
|
||||
cumulative_shares: number
|
||||
next_block_reward_btc: number
|
||||
next_difficulty_percent: number
|
||||
@@ -183,19 +196,59 @@ export const poolStatus = sdk.Action.withoutInput(
|
||||
const stratumPort = ports[0].assigned ?? ports[0].requested
|
||||
|
||||
// Fetch from inside the service's network namespace: temp subcontainers
|
||||
// share it, so curl reaches kamado-api on 127.0.0.1.
|
||||
const { snap, dbg } = await sdk.SubContainer.withTemp(
|
||||
// share it, so curl reaches kamado-api on 127.0.0.1. The ckpool volume
|
||||
// is mounted read-only so the PoW diagnostics below can inspect the
|
||||
// ckpool log directly.
|
||||
const { snap, dbg, pow } = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'main' },
|
||||
null,
|
||||
'pool-status',
|
||||
async (sub) => ({
|
||||
snap: await curlJson<Snapshot>(sub, `${API_BASE}/api/snapshot`),
|
||||
dbg: await curlJson<DebugBlocks>(
|
||||
sub,
|
||||
`${API_BASE}/api/admin/debug-blocks`,
|
||||
),
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'ckpool',
|
||||
subpath: null,
|
||||
mountpoint: ckpoolRoot,
|
||||
readonly: true,
|
||||
}),
|
||||
'pool-status',
|
||||
async (sub) => {
|
||||
// PoW capture pipeline (ckpool patch 0008 → log → kamado-api):
|
||||
// check each link so a break is attributable. grep -a treats the
|
||||
// ckpool binary and log as text; counts are "0" on no match.
|
||||
const countIn = async (target: string): Promise<number> => {
|
||||
const res = await sub.exec([
|
||||
'sh',
|
||||
'-c',
|
||||
`grep -ac "Best share PoW data" ${target} 2>/dev/null || true`,
|
||||
])
|
||||
return parseInt((res.stdout || '').toString().trim(), 10) || 0
|
||||
}
|
||||
const acceptedRes = await sub.exec([
|
||||
'sh',
|
||||
'-c',
|
||||
`grep -ac "Accepted client" ${ckpoolLogFile} 2>/dev/null || true`,
|
||||
])
|
||||
// The newest PoW line verbatim (bounded), so the action can run
|
||||
// the exact same regex + JSON.parse kamado-api uses and report
|
||||
// WHERE ingestion breaks on real data rather than guessing.
|
||||
const lastPowRes = await sub.exec([
|
||||
'sh',
|
||||
'-c',
|
||||
`grep -a "Best share PoW data" ${ckpoolLogFile} 2>/dev/null | tail -1 | cut -c1-8000 || true`,
|
||||
])
|
||||
return {
|
||||
snap: await curlJson<Snapshot>(sub, `${API_BASE}/api/snapshot`),
|
||||
dbg: await curlJson<DebugBlocks>(
|
||||
sub,
|
||||
`${API_BASE}/api/admin/debug-blocks`,
|
||||
),
|
||||
pow: {
|
||||
binaryPatched: (await countIn('/usr/local/bin/ckpool')) > 0,
|
||||
logPowLines: await countIn(ckpoolLogFile),
|
||||
logAcceptedLines:
|
||||
parseInt((acceptedRes.stdout || '').toString().trim(), 10) || 0,
|
||||
lastPowLine: (lastPowRes.stdout || '').toString().trim(),
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const lines: string[] = []
|
||||
@@ -372,6 +425,103 @@ export const poolStatus = sdk.Action.withoutInput(
|
||||
log(' (debug endpoint unavailable)')
|
||||
}
|
||||
|
||||
// ── 1c. PoW capture pipeline ────────────────────────────────────────────
|
||||
// Traces best-share header capture (ckpool patch 0008) link by link:
|
||||
// patched binary → "Best share PoW data" log lines → kamado-api ingest.
|
||||
log('')
|
||||
log('● PoW capture (best-share header inspector)')
|
||||
log(
|
||||
` ckpool binary: ${pow.binaryPatched ? 'patched (0008) ✓' : 'NOT PATCHED — image predates patch 0008'}`,
|
||||
)
|
||||
log(
|
||||
` ckpool log: ${pow.logPowLines} PoW data line${pow.logPowLines === 1 ? '' : 's'}, ${pow.logAcceptedLines} accepted-share lines`,
|
||||
)
|
||||
const snapPow = snap?.best_share_pow
|
||||
log(
|
||||
` kamado-api: ${
|
||||
snapPow
|
||||
? `record held — diff ${fmtDiff(snapPow.sdiff)} at height ${snapPow.height} (${snapPow.seen_at.slice(0, 19)})`
|
||||
: 'no PoW record in snapshot'
|
||||
}`,
|
||||
)
|
||||
// Log-derived session stats prove the tailer reads the log at all —
|
||||
// the share counters above come from ckpool's socket instead.
|
||||
const tailerShares = (snap?.diff_dist_session ?? []).reduce(
|
||||
(a, b) => a + b,
|
||||
0,
|
||||
)
|
||||
log(
|
||||
` log tailer: ${tailerShares} accepted shares ingested this session`,
|
||||
)
|
||||
// Replay kamado-api's parse on the newest real PoW line: decode one
|
||||
// brace-balanced JSON object after the marker, ignoring any trailing
|
||||
// bytes — the same tolerance the Go parser applies.
|
||||
if (pow.lastPowLine) {
|
||||
let parseVerdict: string
|
||||
const start = pow.lastPowLine.indexOf('{', pow.lastPowLine.indexOf('Best share PoW data'))
|
||||
if (start < 0) {
|
||||
parseVerdict = 'NO JSON OBJECT after marker'
|
||||
} else {
|
||||
// Walk to the matching close brace, honouring strings/escapes.
|
||||
let depth = 0
|
||||
let inStr = false
|
||||
let esc = false
|
||||
let end = -1
|
||||
for (let i = start; i < pow.lastPowLine.length; i++) {
|
||||
const c = pow.lastPowLine[i]
|
||||
if (esc) esc = false
|
||||
else if (inStr) {
|
||||
if (c === '\\') esc = true
|
||||
else if (c === '"') inStr = false
|
||||
} else if (c === '"') inStr = true
|
||||
else if (c === '{') depth++
|
||||
else if (c === '}' && --depth === 0) {
|
||||
end = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (end < 0) {
|
||||
parseVerdict = 'UNTERMINATED JSON — line truncated mid-record'
|
||||
} else {
|
||||
try {
|
||||
const j = JSON.parse(pow.lastPowLine.slice(start, end + 1))
|
||||
parseVerdict =
|
||||
typeof j.header === 'string' && j.header.length === 160
|
||||
? `parses ✓ (sdiff ${fmtDiff(j.sdiff ?? 0)}, height ${j.height}, header 160 hex)`
|
||||
: `JSON ok but header field is ${typeof j.header === 'string' ? `${j.header.length} chars` : 'missing'}`
|
||||
} catch (e) {
|
||||
parseVerdict = `JSON PARSE FAILED — ${String(e).slice(0, 120)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
log(` last PoW line: ${parseVerdict}`)
|
||||
log(` head: ${pow.lastPowLine.slice(0, 200)}…`)
|
||||
// Tail rendered via JSON.stringify so control bytes (\r, | ||||