Compare commits
3
Commits
85f61e5c53
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a35b02c7e
|
||
|
|
2c4c6609ba
|
||
|
|
af8357dc63 |
@@ -2,7 +2,7 @@
|
||||
|
||||
A solo Bitcoin mining pool built on a patched fork of [CKPool](https://bitbucket.org/ckolivas/ckpool), with a Go middleware API, real-time Svelte dashboard, and full StartOS integration.
|
||||
|
||||
Kamado exists because existing CKPool wrappers read only a handful of periodic stats files and miss most of CKPool's rich runtime data. Kamado talks directly to CKPool's Unix socket API, subscribes to bitcoind via both RPC and ZMQ, tails CKPool's log for block-solve events, and merges everything into a single live snapshot that the dashboard consumes over WebSocket.
|
||||
Kamado exists because existing CKPool wrappers like Bassin read only a handful of periodic stats files and miss most of CKPool's rich runtime data. Kamado talks directly to CKPool's Unix socket API, subscribes to bitcoind via both RPC and ZMQ, tails CKPool's log for block-solve events, and merges everything into a single live snapshot that the dashboard consumes over WebSocket.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -59,9 +59,11 @@ The Go API (`kamado-api`) bridges CKPool's Unix socket protocol, bitcoind's JSON
|
||||
|
||||
### Stratum TLS
|
||||
|
||||
Optional encrypted stratum via stunnel. CKPool binds two stratum sockets — a public plaintext port and a loopback-only internal port. Stunnel terminates TLS on the external port and forwards decrypted traffic to the internal bind. CKPool tags connections by `serverurl` index (`server === 1` means TLS), which the dashboard reads to display a lock icon next to encrypted miners — no source-IP heuristics needed.
|
||||
Optional encrypted stratum via stunnel. CKPool binds a public plaintext socket plus one loopback-only socket per TLS certificate; stunnel terminates TLS on the external port and forwards decrypted traffic to the matching internal bind. CKPool tags each connection with its `serverurl` index, and the deployment declares what those indices mean via the `STRATUM_SERVERS` env — so the dashboard shows a lock icon next to encrypted miners and names the certificate in use on hover, with no source-IP heuristics.
|
||||
|
||||
The TLS certificate is auto-generated on first start with broad SAN coverage (`.local`, `.embassy`, `.onion`, `.lan`, `.home.arpa`, `.internal`) so miner firmware that validates the SAN against the connection hostname (e.g. AxeOS with mbedtls) works without manual cert pinning. A version marker triggers automatic regeneration when the cert format changes.
|
||||
Under StartOS this drives two certificates on one port, chosen per connection by SNI: a CA-issued (Let's Encrypt) certificate for miners connecting over a clearnet domain, and the self-signed one for miners on the LAN, which send no SNI and fall through to it.
|
||||
|
||||
The self-signed certificate is auto-generated on first start with broad SAN coverage (`.local`, `.embassy`, `.onion`, `.lan`, `.home.arpa`, `.internal`) so miner firmware that validates the SAN against the connection hostname (e.g. AxeOS with mbedtls) works without manual cert pinning. A version marker triggers automatic regeneration when the cert format changes.
|
||||
|
||||
### Dashboard
|
||||
|
||||
@@ -129,7 +131,7 @@ cp .env.example .env # set bitcoind RPC credentials
|
||||
make up # build images + start ckpool + api
|
||||
```
|
||||
|
||||
The dashboard is at `http://localhost:8080`. Point a miner at `stratum+tcp://localhost:3333` with a Bitcoin address as the username.
|
||||
The dashboard is at `http://localhost:8080`. Point a miner at `stratum+tcp://localhost:3333` with a valid Bitcoin address as the username.
|
||||
|
||||
### Make Targets
|
||||
|
||||
@@ -161,6 +163,7 @@ The dashboard is at `http://localhost:8080`. Point a miner at `stratum+tcp://loc
|
||||
| `BITCOIN_ZMQ_BLOCK` | (disabled) | no | ZMQ hashblock endpoint (e.g. `tcp://127.0.0.1:28332`) |
|
||||
| `BITCOIN_RPC_TIMEOUT` | `10s` | no | RPC call timeout |
|
||||
| `MEMPOOL_BASE_URL` | (mempool.space) | no | Custom mempool explorer URL |
|
||||
| `STRATUM_SERVERS` | (undeclared) | no | JSON array describing ckpool's `serverurl[]` binds, e.g. `[{"kind":"plain","label":"Plaintext"},{"kind":"tls-local","label":"TLS — self-signed"}]`. Entry N labels clients with `server == N` in the dashboard. Unset falls back to treating index 1 as TLS |
|
||||
|
||||
### UI Development
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -65,6 +66,18 @@ func main() {
|
||||
agg := state.New(ck, rpc, cfg.PollInterval, log)
|
||||
agg.Store = blockStore
|
||||
agg.MempoolBaseURL = cfg.MempoolBaseURL
|
||||
// Purely descriptive (it drives the dashboard's connection badge), so a
|
||||
// malformed value degrades to the UI's fallback rather than refusing to
|
||||
// start a pool that is otherwise fine.
|
||||
if cfg.StratumServersJSON != "" {
|
||||
var servers []state.StratumServer
|
||||
if err := json.Unmarshal([]byte(cfg.StratumServersJSON), &servers); err != nil {
|
||||
log.Warn("ignoring malformed STRATUM_SERVERS", "err", err)
|
||||
} else {
|
||||
agg.StratumServers = servers
|
||||
log.Info("stratum binds declared", "count", len(servers))
|
||||
}
|
||||
}
|
||||
agg.LogFilePath = cfg.CKPoolLogFile
|
||||
agg.KillCKPool = killCKPool(log)
|
||||
|
||||
|
||||
@@ -39,21 +39,29 @@ type Config struct {
|
||||
// the UI uses the public mempool.space; non-empty means the user
|
||||
// has pointed Kamado at their own instance via the StartOS config.
|
||||
MempoolBaseURL string
|
||||
|
||||
// Optional JSON array describing ckpool's serverurl[] binds, so the
|
||||
// dashboard can name the transport a miner connected over instead of
|
||||
// guessing from the bind index. Rendered by whoever writes
|
||||
// ckpool.conf; empty means "undeclared" and the UI falls back. Parsed
|
||||
// in main (the concrete type lives with the snapshot it belongs to).
|
||||
StratumServersJSON string
|
||||
}
|
||||
|
||||
func FromEnv() (*Config, error) {
|
||||
cfg := &Config{
|
||||
ListenAddr: getenv("LISTEN_ADDR", ":8080"),
|
||||
CKPoolSockDir: getenv("CKPOOL_SOCKDIR", "/run/ckpool"),
|
||||
BitcoinRPCURL: os.Getenv("BITCOIN_RPC_URL"),
|
||||
BitcoinRPCUser: os.Getenv("BITCOIN_RPC_USER"),
|
||||
ListenAddr: getenv("LISTEN_ADDR", ":8080"),
|
||||
CKPoolSockDir: getenv("CKPOOL_SOCKDIR", "/run/ckpool"),
|
||||
BitcoinRPCURL: os.Getenv("BITCOIN_RPC_URL"),
|
||||
BitcoinRPCUser: os.Getenv("BITCOIN_RPC_USER"),
|
||||
BitcoinRPCPassword: os.Getenv("BITCOIN_RPC_PASSWORD"),
|
||||
BitcoinRPCTimeout: getenvDuration("BITCOIN_RPC_TIMEOUT", 10*time.Second),
|
||||
BitcoinZMQBlock: os.Getenv("BITCOIN_ZMQ_BLOCK"),
|
||||
CKPoolLogFile: getenv("CKPOOL_LOGFILE", "/var/log/ckpool/ckpool.log"),
|
||||
DBPath: getenv("DB_PATH", "/var/lib/kamado/kamado.db"),
|
||||
PollInterval: getenvDuration("POLL_INTERVAL", 5*time.Second),
|
||||
MempoolBaseURL: os.Getenv("MEMPOOL_BASE_URL"),
|
||||
BitcoinRPCTimeout: getenvDuration("BITCOIN_RPC_TIMEOUT", 10*time.Second),
|
||||
BitcoinZMQBlock: os.Getenv("BITCOIN_ZMQ_BLOCK"),
|
||||
CKPoolLogFile: getenv("CKPOOL_LOGFILE", "/var/log/ckpool/ckpool.log"),
|
||||
DBPath: getenv("DB_PATH", "/var/lib/kamado/kamado.db"),
|
||||
PollInterval: getenvDuration("POLL_INTERVAL", 5*time.Second),
|
||||
MempoolBaseURL: os.Getenv("MEMPOOL_BASE_URL"),
|
||||
StratumServersJSON: os.Getenv("STRATUM_SERVERS"),
|
||||
}
|
||||
|
||||
if cfg.BitcoinRPCURL == "" {
|
||||
|
||||
@@ -25,6 +25,21 @@ type HashratePoint struct {
|
||||
V float64 `json:"v"` // H/s
|
||||
}
|
||||
|
||||
// StratumServer describes one entry in ckpool's serverurl[] array. ckpool
|
||||
// tags every client with the index of the bind it arrived on, but the index
|
||||
// alone doesn't say what that bind *is* — that depends on how the deployment
|
||||
// rendered ckpool.conf. Declaring the array lets the dashboard report the
|
||||
// actual transport (plaintext, TLS with which certificate) instead of
|
||||
// hardcoding a bind order.
|
||||
type StratumServer struct {
|
||||
// Kind is the stable machine-readable tag the UI switches on:
|
||||
// "plain", "tls-local" (package-managed self-signed certificate) or
|
||||
// "tls-public" (CA-issued certificate for a public domain).
|
||||
Kind string `json:"kind"`
|
||||
// Label is the human-readable description shown on hover.
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// Snapshot is the merged view served to the UI. All fields are safe to
|
||||
// JSON-serialize directly.
|
||||
type Snapshot struct {
|
||||
@@ -38,7 +53,7 @@ type Snapshot struct {
|
||||
Clients []ckpool.StratumClient `json:"clients"`
|
||||
|
||||
// Derived/enriched fields
|
||||
HashrateHs float64 `json:"hashrate_hs_1m"` // from PoolStats.DSPS1
|
||||
HashrateHs float64 `json:"hashrate_hs_1m"` // from PoolStats.DSPS1
|
||||
HashrateHs5m float64 `json:"hashrate_hs_5m"`
|
||||
HashrateHs1h float64 `json:"hashrate_hs_1h"`
|
||||
HashrateHs24h float64 `json:"hashrate_hs_24h"`
|
||||
@@ -68,8 +83,8 @@ type Snapshot struct {
|
||||
NextDifficultyPercent float64 `json:"next_difficulty_percent"`
|
||||
|
||||
// Bitcoin Core fields
|
||||
Chain *bitcoind.BlockchainInfo `json:"chain"`
|
||||
NetworkHashrateHs float64 `json:"network_hashrate_hs"`
|
||||
Chain *bitcoind.BlockchainInfo `json:"chain"`
|
||||
NetworkHashrateHs float64 `json:"network_hashrate_hs"`
|
||||
|
||||
// Recent found blocks (in-memory history; persisted in Phase 2b.5).
|
||||
RecentBlocks []BlockRecord `json:"recent_blocks,omitempty"`
|
||||
@@ -82,6 +97,13 @@ type Snapshot struct {
|
||||
// the StartOS config "Block Explorer" -> "Custom URL".
|
||||
MempoolBaseURL string `json:"mempool_base_url,omitempty"`
|
||||
|
||||
// Describes ckpool's serverurl[] array: entry N tells the UI what a
|
||||
// client with `server == N` is actually connected over. Set via the
|
||||
// STRATUM_SERVERS env by whoever renders ckpool.conf (the StartOS
|
||||
// wrapper). Empty when undeclared, in which case the UI falls back to
|
||||
// its historical "index 1 means TLS" assumption.
|
||||
StratumServers []StratumServer `json:"stratum_servers,omitempty"`
|
||||
|
||||
// Counts of share-submit attempts ("Possible/Submitting block solve"
|
||||
// log lines) and confirmed solves ("Solved and confirmed block").
|
||||
// A growing gap means bitcoind is rejecting our submissions or
|
||||
@@ -93,15 +115,15 @@ type Snapshot struct {
|
||||
// LastZMQEventAge is the seconds-since the last bitcoind hashblock
|
||||
// frame arrived; -1 means no event seen since startup. ZMQEnabled
|
||||
// is whether the user configured an endpoint at all.
|
||||
ZMQEnabled bool `json:"zmq_enabled"`
|
||||
LastZMQEventAge float64 `json:"last_zmq_event_age,omitempty"` // seconds; >=0
|
||||
HasLastZMQEvent bool `json:"has_last_zmq_event"`
|
||||
TipChangedAge float64 `json:"tip_changed_age"` // seconds since tip height last changed
|
||||
ZMQEnabled bool `json:"zmq_enabled"`
|
||||
LastZMQEventAge float64 `json:"last_zmq_event_age,omitempty"` // seconds; >=0
|
||||
HasLastZMQEvent bool `json:"has_last_zmq_event"`
|
||||
TipChangedAge float64 `json:"tip_changed_age"` // seconds since tip height last changed
|
||||
|
||||
// Bitcoin Core peer connections.
|
||||
PeerCount int `json:"peer_count"`
|
||||
PeersIn int `json:"peers_in"`
|
||||
PeersOut int `json:"peers_out"`
|
||||
PeerCount int `json:"peer_count"`
|
||||
PeersIn int `json:"peers_in"`
|
||||
PeersOut int `json:"peers_out"`
|
||||
|
||||
// Share counters: raw counts (1 submission = 1 share regardless of diff).
|
||||
// Session = since ckpool started; AllTime = persisted across restarts.
|
||||
@@ -151,8 +173,8 @@ const (
|
||||
kvLatencyLastMs = "latency_last_ms"
|
||||
kvStaleWorkHashes = "stale_work_hashes"
|
||||
|
||||
kvAckedBestDiff = "acked_best_diff"
|
||||
kvBestDiff = "best_diff"
|
||||
kvAckedBestDiff = "acked_best_diff"
|
||||
kvBestDiff = "best_diff"
|
||||
kvBestShareHash = "best_share_hash"
|
||||
kvBestShareNetDiff = "best_share_net_diff"
|
||||
|
||||
@@ -186,6 +208,10 @@ type Aggregator struct {
|
||||
// MempoolBaseURL leaves the UI on its mempool.space defaults.
|
||||
MempoolBaseURL string
|
||||
|
||||
// Declared meaning of each ckpool serverurl[] index; see StratumServer.
|
||||
// Nil leaves the UI on its index-1-means-TLS fallback.
|
||||
StratumServers []StratumServer
|
||||
|
||||
// LogFilePath is the ckpool log path, used for one-time backfill
|
||||
// of the best share hash when upgrading from a version that didn't
|
||||
// capture it. Set by main before calling Run.
|
||||
@@ -220,10 +246,10 @@ type Aggregator struct {
|
||||
// post-load observation of pool.Shares only establishes the
|
||||
// baseline — the current ckpool counter was already accounted for
|
||||
// before we crashed.
|
||||
cumulativeShares float64
|
||||
lastPoolShares float64
|
||||
hasPoolSharesBaseline bool
|
||||
lastCumulativeSave time.Time
|
||||
cumulativeShares float64
|
||||
lastPoolShares float64
|
||||
hasPoolSharesBaseline bool
|
||||
lastCumulativeSave time.Time
|
||||
|
||||
// Next-block reward cache; refreshed on its own cadence so we don't
|
||||
// hammer bitcoind with getblocktemplate on every poll tick.
|
||||
@@ -245,8 +271,8 @@ type Aggregator struct {
|
||||
// btcFailStreak counts consecutive refreshes where bitcoind was
|
||||
// unreachable. After a threshold, if no block submission is pending,
|
||||
// we kill the ckpool process so miners can failover to other pools.
|
||||
btcFailStreak int
|
||||
ckpoolKilled bool // true after we sent SIGTERM, reset on bitcoind recovery
|
||||
btcFailStreak int
|
||||
ckpoolKilled bool // true after we sent SIGTERM, reset on bitcoind recovery
|
||||
|
||||
// readyOnce + ready closes the Ready() channel exactly once after
|
||||
// the first refresh completes. main blocks briefly on this so the
|
||||
@@ -401,7 +427,11 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
next := Snapshot{GeneratedAt: time.Now(), MempoolBaseURL: a.MempoolBaseURL}
|
||||
next := Snapshot{
|
||||
GeneratedAt: time.Now(),
|
||||
MempoolBaseURL: a.MempoolBaseURL,
|
||||
StratumServers: a.StratumServers,
|
||||
}
|
||||
|
||||
// --- ckpool: poolstats, users, workers, clients, uptime ---
|
||||
if ps, err := a.CK.PoolStats(ctx); err == nil {
|
||||
@@ -776,7 +806,6 @@ func diffBucket(d float64) int {
|
||||
// DiffBucketLabels are the human-readable labels for each difficulty bucket.
|
||||
var DiffBucketLabels = [6]string{"< 1M", "1M – 100M", "100M – 1G", "1G – 100G", "100G – 1T", "≥ 1T"}
|
||||
|
||||
|
||||
// IngestShareEvents reads individual share events from the log tailer
|
||||
// and maintains rejection-reason counts and difficulty-distribution
|
||||
// histograms. Session counters reset when ckpool restarts (detected by
|
||||
|
||||
@@ -49,6 +49,11 @@ LOGDIR="${LOGDIR:-/var/log/ckpool}"
|
||||
SOCKET_DIR="${SOCKET_DIR:-/run/ckpool}"
|
||||
SHARE_LOG="${SHARE_LOG:-1}"
|
||||
CKPOOL_LOGLEVEL="${CKPOOL_LOGLEVEL:-6}"
|
||||
# ckpool's second, loopback-only stratum bind. Nothing terminates TLS in the
|
||||
# dev compose stack, but the bind must still resolve: ckpool passes the port
|
||||
# straight to getaddrinfo() and a non-numeric service string is fatal
|
||||
# (connector.c logs "Failed to extract resolved url" and exit(1)s).
|
||||
TLS_INTERNAL_PORT="${TLS_INTERNAL_PORT:-3437}"
|
||||
|
||||
mkdir -p "$LOGDIR" "$SOCKET_DIR"
|
||||
|
||||
@@ -68,6 +73,7 @@ sed \
|
||||
-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" \
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Formatters for hashrate, share difficulty, time, and miner hardware
|
||||
// detection from stratum user-agent strings. Pure functions, no DOM.
|
||||
|
||||
import type { StratumServer } from "./types";
|
||||
|
||||
const HASHRATE_UNITS = ["H/s", "kH/s", "MH/s", "GH/s", "TH/s", "PH/s", "EH/s"];
|
||||
|
||||
export function formatHashrate(hs: number): string {
|
||||
@@ -100,6 +102,26 @@ export function explorerBaseFor(
|
||||
return "https://mempool.space";
|
||||
}
|
||||
|
||||
// Resolve how a miner is connected from its ckpool serverurl[] index.
|
||||
//
|
||||
// The deployment declares the bind array via STRATUM_SERVERS (the StartOS
|
||||
// wrapper renders one entry per bind it configures). When it hasn't — the
|
||||
// dev compose stack, or an older wrapper — fall back to the historical
|
||||
// layout, where index 0 is the plaintext bind and anything else is the
|
||||
// loopback bind that stunnel forwards TLS traffic to.
|
||||
export function connectionOf(
|
||||
serverIndex: number | undefined,
|
||||
servers: StratumServer[] | undefined,
|
||||
): { tls: boolean; label: string } {
|
||||
const declared = servers?.[serverIndex ?? 0];
|
||||
if (declared) {
|
||||
return { tls: declared.kind !== "plain", label: declared.label };
|
||||
}
|
||||
return serverIndex
|
||||
? { tls: true, label: "Encrypted connection via TLS" }
|
||||
: { tls: false, label: "Unencrypted connection" };
|
||||
}
|
||||
|
||||
// Detect common Bitcoin mining hardware from the stratum useragent
|
||||
// string. This is a best-effort heuristic; unknown agents fall back
|
||||
// to a stripped version of the raw string. Order matters — check
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
detectHardware,
|
||||
isOpenSource,
|
||||
btcAddressOf,
|
||||
connectionOf,
|
||||
} from "../format";
|
||||
import type { StratumClient, Worker } from "../types";
|
||||
|
||||
@@ -18,6 +19,7 @@
|
||||
hardware: string;
|
||||
openSource: boolean;
|
||||
tls: boolean;
|
||||
tlsLabel: string;
|
||||
hashrate1m: number;
|
||||
hashrate1h: number;
|
||||
bestSession: number;
|
||||
@@ -35,9 +37,10 @@
|
||||
// from the joined worker_instance, which ckpool keys by full
|
||||
// workername with user = the BTC.
|
||||
//
|
||||
// ckpool's serverurl array gives us a second piece of info: TLS
|
||||
// traffic comes in via stunnel on the loopback-only bind (server
|
||||
// index 1), so client.server === 1 means this miner is using TLS.
|
||||
// ckpool's serverurl array gives us a second piece of info: which
|
||||
// bind the connection arrived on. connectionOf() maps that index to
|
||||
// the transport the deployment declared, so the lock badge can name
|
||||
// the certificate in play rather than just "encrypted".
|
||||
const rows = $derived.by<Row[]>(() => {
|
||||
const clients = snap.data?.clients ?? [];
|
||||
const workers = snap.data?.workers ?? [];
|
||||
@@ -52,13 +55,15 @@
|
||||
seen.add(wname);
|
||||
const w = byWorker.get(wname);
|
||||
const btcAddress = w?.user ?? btcAddressOf(wname);
|
||||
const conn = connectionOf(c.server, snap.data?.stratum_servers);
|
||||
out.push({
|
||||
workerName: wname,
|
||||
btcAddress,
|
||||
sourceIp: c.address,
|
||||
hardware: detectHardware(c.useragent),
|
||||
openSource: isOpenSource(c.useragent),
|
||||
tls: c.server === 1,
|
||||
tls: conn.tls,
|
||||
tlsLabel: conn.label,
|
||||
hashrate1m: c.dsps1 * 2 ** 32,
|
||||
hashrate1h: c.dsps60 * 2 ** 32,
|
||||
bestSession: c.bestdiff,
|
||||
@@ -79,6 +84,7 @@
|
||||
hardware: "offline",
|
||||
openSource: false,
|
||||
tls: false,
|
||||
tlsLabel: "",
|
||||
hashrate1m: 0,
|
||||
hashrate1h: 0,
|
||||
bestSession: 0,
|
||||
@@ -133,7 +139,7 @@
|
||||
title="View per-worker stats"
|
||||
>{r.workerName}</button>
|
||||
{#if r.tls}
|
||||
<span class="badge tls" title="Encrypted connection via TLS">
|
||||
<span class="badge tls" title={r.tlsLabel}>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H4a1.5 1.5 0 0 0-1.5 1.5v5A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 12 7h-.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6H6V4.5a2 2 0 1 1 4 0V7Z"/>
|
||||
</svg>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
detectHardware,
|
||||
isOpenSource,
|
||||
explorerBaseFor,
|
||||
connectionOf,
|
||||
} from "../format";
|
||||
import type { Worker, StratumClient } from "../types";
|
||||
|
||||
@@ -46,6 +47,7 @@
|
||||
hardware: string;
|
||||
openSource: boolean;
|
||||
tls: boolean;
|
||||
tlsLabel: string;
|
||||
sourceIp: string;
|
||||
hashrate1m: number;
|
||||
hashrate1h: number;
|
||||
@@ -64,7 +66,10 @@
|
||||
worker: w.worker,
|
||||
hardware: c ? detectHardware(c.useragent) : "offline",
|
||||
openSource: c ? isOpenSource(c.useragent) : false,
|
||||
tls: c?.server === 1,
|
||||
tls: !!c && connectionOf(c.server, snap.data?.stratum_servers).tls,
|
||||
tlsLabel: c
|
||||
? connectionOf(c.server, snap.data?.stratum_servers).label
|
||||
: "",
|
||||
sourceIp: c?.address ?? "",
|
||||
hashrate1m: c ? c.dsps1 * 2 ** 32 : 0,
|
||||
hashrate1h: c ? c.dsps60 * 2 ** 32 : 0,
|
||||
@@ -85,7 +90,8 @@
|
||||
worker: wname,
|
||||
hardware: detectHardware(c.useragent),
|
||||
openSource: isOpenSource(c.useragent),
|
||||
tls: c.server === 1,
|
||||
tls: connectionOf(c.server, snap.data?.stratum_servers).tls,
|
||||
tlsLabel: connectionOf(c.server, snap.data?.stratum_servers).label,
|
||||
sourceIp: c.address,
|
||||
hashrate1m: c.dsps1 * 2 ** 32,
|
||||
hashrate1h: c.dsps60 * 2 ** 32,
|
||||
@@ -232,7 +238,7 @@
|
||||
title="View per-worker stats"
|
||||
>{r.worker}</button>
|
||||
{#if r.tls}
|
||||
<span class="badge tls" title="Encrypted connection via TLS">
|
||||
<span class="badge tls" title={r.tlsLabel}>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H4a1.5 1.5 0 0 0-1.5 1.5v5A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 12 7h-.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6H6V4.5a2 2 0 1 1 4 0V7Z"/>
|
||||
</svg>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
detectHardware,
|
||||
isOpenSource,
|
||||
btcAddressOf,
|
||||
connectionOf,
|
||||
} from "../format";
|
||||
import type { Worker, StratumClient } from "../types";
|
||||
|
||||
@@ -29,7 +30,12 @@
|
||||
const idle = $derived(client?.idle ?? worker?.idle ?? false);
|
||||
const hardware = $derived(client ? detectHardware(client.useragent) : "offline");
|
||||
const openSource = $derived(client ? isOpenSource(client.useragent) : false);
|
||||
const tls = $derived(client?.server === 1);
|
||||
const conn = $derived(
|
||||
client
|
||||
? connectionOf(client.server, snap.data?.stratum_servers)
|
||||
: { tls: false, label: "" },
|
||||
);
|
||||
const tls = $derived(conn.tls);
|
||||
|
||||
const hs1m = $derived(client ? client.dsps1 * 2 ** 32 : 0);
|
||||
const hs5m = $derived(client ? client.dsps5 * 2 ** 32 : 0);
|
||||
@@ -88,7 +94,7 @@
|
||||
title="View per-user stats"
|
||||
>{btcAddress}</button>
|
||||
{#if tls}
|
||||
<span class="badge tls" title="Encrypted connection via TLS">
|
||||
<span class="badge tls" title={conn.label}>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H4a1.5 1.5 0 0 0-1.5 1.5v5A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 12 7h-.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6H6V4.5a2 2 0 1 1 4 0V7Z"/>
|
||||
</svg>
|
||||
|
||||
+18
-3
@@ -71,9 +71,10 @@ export type StratumClient = {
|
||||
workername: string;
|
||||
userid: number;
|
||||
// Index into ckpool's serverurl[] array; identifies which stratum
|
||||
// bind the client connected on. We use this to tag TLS clients:
|
||||
// index 0 is the public plaintext bind, index 1 is the loopback-only
|
||||
// bind that stunnel forwards TLS traffic to.
|
||||
// bind the client connected on. What each index *means* depends on
|
||||
// how the deployment rendered ckpool.conf, so resolve it through
|
||||
// Snapshot.stratum_servers rather than hardcoding — see
|
||||
// connectionOf() in format.ts.
|
||||
server: number;
|
||||
bestdiff: number;
|
||||
};
|
||||
@@ -112,6 +113,16 @@ export type HashratePoint = {
|
||||
v: number; // H/s
|
||||
};
|
||||
|
||||
// One entry per ckpool serverurl[] bind, declared by the deployment via
|
||||
// the STRATUM_SERVERS env. `kind` is the stable tag the UI switches on;
|
||||
// `label` is free text shown on hover.
|
||||
export type StratumServerKind = "plain" | "tls-local" | "tls-public";
|
||||
|
||||
export type StratumServer = {
|
||||
kind: StratumServerKind;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type Snapshot = {
|
||||
generated_at: string;
|
||||
pool: PoolStats | null;
|
||||
@@ -137,6 +148,10 @@ export type Snapshot = {
|
||||
// Optional override for explorer links. Empty/undefined means the
|
||||
// UI falls back to its mempool.space defaults.
|
||||
mempool_base_url?: string;
|
||||
// Describes ckpool's serverurl[] binds: entry N says what a client
|
||||
// with `server === N` is connected over. Undefined when the
|
||||
// deployment didn't declare them — see connectionOf() in format.ts.
|
||||
stratum_servers?: StratumServer[];
|
||||
// Submission attempt tracking — count of "Possible block solve"
|
||||
// log lines vs "Solved and confirmed" lines. A growing gap means
|
||||
// submissions are being rejected by bitcoind.
|
||||
|
||||
Reference in New Issue
Block a user