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.
|
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
|
## Architecture
|
||||||
|
|
||||||
@@ -59,9 +59,11 @@ The Go API (`kamado-api`) bridges CKPool's Unix socket protocol, bitcoind's JSON
|
|||||||
|
|
||||||
### Stratum TLS
|
### 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
|
### Dashboard
|
||||||
|
|
||||||
@@ -129,7 +131,7 @@ cp .env.example .env # set bitcoind RPC credentials
|
|||||||
make up # build images + start ckpool + api
|
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
|
### 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_ZMQ_BLOCK` | (disabled) | no | ZMQ hashblock endpoint (e.g. `tcp://127.0.0.1:28332`) |
|
||||||
| `BITCOIN_RPC_TIMEOUT` | `10s` | no | RPC call timeout |
|
| `BITCOIN_RPC_TIMEOUT` | `10s` | no | RPC call timeout |
|
||||||
| `MEMPOOL_BASE_URL` | (mempool.space) | no | Custom mempool explorer URL |
|
| `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
|
### UI Development
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -65,6 +66,18 @@ func main() {
|
|||||||
agg := state.New(ck, rpc, cfg.PollInterval, log)
|
agg := state.New(ck, rpc, cfg.PollInterval, log)
|
||||||
agg.Store = blockStore
|
agg.Store = blockStore
|
||||||
agg.MempoolBaseURL = cfg.MempoolBaseURL
|
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.LogFilePath = cfg.CKPoolLogFile
|
||||||
agg.KillCKPool = killCKPool(log)
|
agg.KillCKPool = killCKPool(log)
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ type Config struct {
|
|||||||
// the UI uses the public mempool.space; non-empty means the user
|
// the UI uses the public mempool.space; non-empty means the user
|
||||||
// has pointed Kamado at their own instance via the StartOS config.
|
// has pointed Kamado at their own instance via the StartOS config.
|
||||||
MempoolBaseURL string
|
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) {
|
func FromEnv() (*Config, error) {
|
||||||
@@ -54,6 +61,7 @@ func FromEnv() (*Config, error) {
|
|||||||
DBPath: getenv("DB_PATH", "/var/lib/kamado/kamado.db"),
|
DBPath: getenv("DB_PATH", "/var/lib/kamado/kamado.db"),
|
||||||
PollInterval: getenvDuration("POLL_INTERVAL", 5*time.Second),
|
PollInterval: getenvDuration("POLL_INTERVAL", 5*time.Second),
|
||||||
MempoolBaseURL: os.Getenv("MEMPOOL_BASE_URL"),
|
MempoolBaseURL: os.Getenv("MEMPOOL_BASE_URL"),
|
||||||
|
StratumServersJSON: os.Getenv("STRATUM_SERVERS"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.BitcoinRPCURL == "" {
|
if cfg.BitcoinRPCURL == "" {
|
||||||
|
|||||||
@@ -25,6 +25,21 @@ type HashratePoint struct {
|
|||||||
V float64 `json:"v"` // H/s
|
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
|
// Snapshot is the merged view served to the UI. All fields are safe to
|
||||||
// JSON-serialize directly.
|
// JSON-serialize directly.
|
||||||
type Snapshot struct {
|
type Snapshot struct {
|
||||||
@@ -82,6 +97,13 @@ type Snapshot struct {
|
|||||||
// the StartOS config "Block Explorer" -> "Custom URL".
|
// the StartOS config "Block Explorer" -> "Custom URL".
|
||||||
MempoolBaseURL string `json:"mempool_base_url,omitempty"`
|
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"
|
// Counts of share-submit attempts ("Possible/Submitting block solve"
|
||||||
// log lines) and confirmed solves ("Solved and confirmed block").
|
// log lines) and confirmed solves ("Solved and confirmed block").
|
||||||
// A growing gap means bitcoind is rejecting our submissions or
|
// A growing gap means bitcoind is rejecting our submissions or
|
||||||
@@ -186,6 +208,10 @@ type Aggregator struct {
|
|||||||
// MempoolBaseURL leaves the UI on its mempool.space defaults.
|
// MempoolBaseURL leaves the UI on its mempool.space defaults.
|
||||||
MempoolBaseURL string
|
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
|
// LogFilePath is the ckpool log path, used for one-time backfill
|
||||||
// of the best share hash when upgrading from a version that didn't
|
// of the best share hash when upgrading from a version that didn't
|
||||||
// capture it. Set by main before calling Run.
|
// capture it. Set by main before calling Run.
|
||||||
@@ -401,7 +427,11 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
|||||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
defer cancel()
|
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 ---
|
// --- ckpool: poolstats, users, workers, clients, uptime ---
|
||||||
if ps, err := a.CK.PoolStats(ctx); err == nil {
|
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.
|
// DiffBucketLabels are the human-readable labels for each difficulty bucket.
|
||||||
var DiffBucketLabels = [6]string{"< 1M", "1M – 100M", "100M – 1G", "1G – 100G", "100G – 1T", "≥ 1T"}
|
var DiffBucketLabels = [6]string{"< 1M", "1M – 100M", "100M – 1G", "1G – 100G", "100G – 1T", "≥ 1T"}
|
||||||
|
|
||||||
|
|
||||||
// IngestShareEvents reads individual share events from the log tailer
|
// IngestShareEvents reads individual share events from the log tailer
|
||||||
// and maintains rejection-reason counts and difficulty-distribution
|
// and maintains rejection-reason counts and difficulty-distribution
|
||||||
// histograms. Session counters reset when ckpool restarts (detected by
|
// histograms. Session counters reset when ckpool restarts (detected by
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ LOGDIR="${LOGDIR:-/var/log/ckpool}"
|
|||||||
SOCKET_DIR="${SOCKET_DIR:-/run/ckpool}"
|
SOCKET_DIR="${SOCKET_DIR:-/run/ckpool}"
|
||||||
SHARE_LOG="${SHARE_LOG:-1}"
|
SHARE_LOG="${SHARE_LOG:-1}"
|
||||||
CKPOOL_LOGLEVEL="${CKPOOL_LOGLEVEL:-6}"
|
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"
|
mkdir -p "$LOGDIR" "$SOCKET_DIR"
|
||||||
|
|
||||||
@@ -68,6 +73,7 @@ sed \
|
|||||||
-e "s|\${BLOCKPOLL_MS}|${BLOCKPOLL_MS}|g" \
|
-e "s|\${BLOCKPOLL_MS}|${BLOCKPOLL_MS}|g" \
|
||||||
-e "s|\${UPDATE_INTERVAL_S}|${UPDATE_INTERVAL_S}|g" \
|
-e "s|\${UPDATE_INTERVAL_S}|${UPDATE_INTERVAL_S}|g" \
|
||||||
-e "s|\${STRATUM_PORT}|${STRATUM_PORT}|g" \
|
-e "s|\${STRATUM_PORT}|${STRATUM_PORT}|g" \
|
||||||
|
-e "s|\${TLS_INTERNAL_PORT}|${TLS_INTERNAL_PORT}|g" \
|
||||||
-e "s|\${MINDIFF}|${MINDIFF}|g" \
|
-e "s|\${MINDIFF}|${MINDIFF}|g" \
|
||||||
-e "s|\${STARTDIFF}|${STARTDIFF}|g" \
|
-e "s|\${STARTDIFF}|${STARTDIFF}|g" \
|
||||||
-e "s|\${MAXDIFF}|${MAXDIFF}|g" \
|
-e "s|\${MAXDIFF}|${MAXDIFF}|g" \
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Formatters for hashrate, share difficulty, time, and miner hardware
|
// Formatters for hashrate, share difficulty, time, and miner hardware
|
||||||
// detection from stratum user-agent strings. Pure functions, no DOM.
|
// 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"];
|
const HASHRATE_UNITS = ["H/s", "kH/s", "MH/s", "GH/s", "TH/s", "PH/s", "EH/s"];
|
||||||
|
|
||||||
export function formatHashrate(hs: number): string {
|
export function formatHashrate(hs: number): string {
|
||||||
@@ -100,6 +102,26 @@ export function explorerBaseFor(
|
|||||||
return "https://mempool.space";
|
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
|
// Detect common Bitcoin mining hardware from the stratum useragent
|
||||||
// string. This is a best-effort heuristic; unknown agents fall back
|
// string. This is a best-effort heuristic; unknown agents fall back
|
||||||
// to a stripped version of the raw string. Order matters — check
|
// to a stripped version of the raw string. Order matters — check
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
detectHardware,
|
detectHardware,
|
||||||
isOpenSource,
|
isOpenSource,
|
||||||
btcAddressOf,
|
btcAddressOf,
|
||||||
|
connectionOf,
|
||||||
} from "../format";
|
} from "../format";
|
||||||
import type { StratumClient, Worker } from "../types";
|
import type { StratumClient, Worker } from "../types";
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
hardware: string;
|
hardware: string;
|
||||||
openSource: boolean;
|
openSource: boolean;
|
||||||
tls: boolean;
|
tls: boolean;
|
||||||
|
tlsLabel: string;
|
||||||
hashrate1m: number;
|
hashrate1m: number;
|
||||||
hashrate1h: number;
|
hashrate1h: number;
|
||||||
bestSession: number;
|
bestSession: number;
|
||||||
@@ -35,9 +37,10 @@
|
|||||||
// from the joined worker_instance, which ckpool keys by full
|
// from the joined worker_instance, which ckpool keys by full
|
||||||
// workername with user = the BTC.
|
// workername with user = the BTC.
|
||||||
//
|
//
|
||||||
// ckpool's serverurl array gives us a second piece of info: TLS
|
// ckpool's serverurl array gives us a second piece of info: which
|
||||||
// traffic comes in via stunnel on the loopback-only bind (server
|
// bind the connection arrived on. connectionOf() maps that index to
|
||||||
// index 1), so client.server === 1 means this miner is using TLS.
|
// 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 rows = $derived.by<Row[]>(() => {
|
||||||
const clients = snap.data?.clients ?? [];
|
const clients = snap.data?.clients ?? [];
|
||||||
const workers = snap.data?.workers ?? [];
|
const workers = snap.data?.workers ?? [];
|
||||||
@@ -52,13 +55,15 @@
|
|||||||
seen.add(wname);
|
seen.add(wname);
|
||||||
const w = byWorker.get(wname);
|
const w = byWorker.get(wname);
|
||||||
const btcAddress = w?.user ?? btcAddressOf(wname);
|
const btcAddress = w?.user ?? btcAddressOf(wname);
|
||||||
|
const conn = connectionOf(c.server, snap.data?.stratum_servers);
|
||||||
out.push({
|
out.push({
|
||||||
workerName: wname,
|
workerName: wname,
|
||||||
btcAddress,
|
btcAddress,
|
||||||
sourceIp: c.address,
|
sourceIp: c.address,
|
||||||
hardware: detectHardware(c.useragent),
|
hardware: detectHardware(c.useragent),
|
||||||
openSource: isOpenSource(c.useragent),
|
openSource: isOpenSource(c.useragent),
|
||||||
tls: c.server === 1,
|
tls: conn.tls,
|
||||||
|
tlsLabel: conn.label,
|
||||||
hashrate1m: c.dsps1 * 2 ** 32,
|
hashrate1m: c.dsps1 * 2 ** 32,
|
||||||
hashrate1h: c.dsps60 * 2 ** 32,
|
hashrate1h: c.dsps60 * 2 ** 32,
|
||||||
bestSession: c.bestdiff,
|
bestSession: c.bestdiff,
|
||||||
@@ -79,6 +84,7 @@
|
|||||||
hardware: "offline",
|
hardware: "offline",
|
||||||
openSource: false,
|
openSource: false,
|
||||||
tls: false,
|
tls: false,
|
||||||
|
tlsLabel: "",
|
||||||
hashrate1m: 0,
|
hashrate1m: 0,
|
||||||
hashrate1h: 0,
|
hashrate1h: 0,
|
||||||
bestSession: 0,
|
bestSession: 0,
|
||||||
@@ -133,7 +139,7 @@
|
|||||||
title="View per-worker stats"
|
title="View per-worker stats"
|
||||||
>{r.workerName}</button>
|
>{r.workerName}</button>
|
||||||
{#if r.tls}
|
{#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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
detectHardware,
|
detectHardware,
|
||||||
isOpenSource,
|
isOpenSource,
|
||||||
explorerBaseFor,
|
explorerBaseFor,
|
||||||
|
connectionOf,
|
||||||
} from "../format";
|
} from "../format";
|
||||||
import type { Worker, StratumClient } from "../types";
|
import type { Worker, StratumClient } from "../types";
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
hardware: string;
|
hardware: string;
|
||||||
openSource: boolean;
|
openSource: boolean;
|
||||||
tls: boolean;
|
tls: boolean;
|
||||||
|
tlsLabel: string;
|
||||||
sourceIp: string;
|
sourceIp: string;
|
||||||
hashrate1m: number;
|
hashrate1m: number;
|
||||||
hashrate1h: number;
|
hashrate1h: number;
|
||||||
@@ -64,7 +66,10 @@
|
|||||||
worker: w.worker,
|
worker: w.worker,
|
||||||
hardware: c ? detectHardware(c.useragent) : "offline",
|
hardware: c ? detectHardware(c.useragent) : "offline",
|
||||||
openSource: c ? isOpenSource(c.useragent) : false,
|
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 ?? "",
|
sourceIp: c?.address ?? "",
|
||||||
hashrate1m: c ? c.dsps1 * 2 ** 32 : 0,
|
hashrate1m: c ? c.dsps1 * 2 ** 32 : 0,
|
||||||
hashrate1h: c ? c.dsps60 * 2 ** 32 : 0,
|
hashrate1h: c ? c.dsps60 * 2 ** 32 : 0,
|
||||||
@@ -85,7 +90,8 @@
|
|||||||
worker: wname,
|
worker: wname,
|
||||||
hardware: detectHardware(c.useragent),
|
hardware: detectHardware(c.useragent),
|
||||||
openSource: isOpenSource(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,
|
sourceIp: c.address,
|
||||||
hashrate1m: c.dsps1 * 2 ** 32,
|
hashrate1m: c.dsps1 * 2 ** 32,
|
||||||
hashrate1h: c.dsps60 * 2 ** 32,
|
hashrate1h: c.dsps60 * 2 ** 32,
|
||||||
@@ -232,7 +238,7 @@
|
|||||||
title="View per-worker stats"
|
title="View per-worker stats"
|
||||||
>{r.worker}</button>
|
>{r.worker}</button>
|
||||||
{#if r.tls}
|
{#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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
detectHardware,
|
detectHardware,
|
||||||
isOpenSource,
|
isOpenSource,
|
||||||
btcAddressOf,
|
btcAddressOf,
|
||||||
|
connectionOf,
|
||||||
} from "../format";
|
} from "../format";
|
||||||
import type { Worker, StratumClient } from "../types";
|
import type { Worker, StratumClient } from "../types";
|
||||||
|
|
||||||
@@ -29,7 +30,12 @@
|
|||||||
const idle = $derived(client?.idle ?? worker?.idle ?? false);
|
const idle = $derived(client?.idle ?? worker?.idle ?? false);
|
||||||
const hardware = $derived(client ? detectHardware(client.useragent) : "offline");
|
const hardware = $derived(client ? detectHardware(client.useragent) : "offline");
|
||||||
const openSource = $derived(client ? isOpenSource(client.useragent) : false);
|
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 hs1m = $derived(client ? client.dsps1 * 2 ** 32 : 0);
|
||||||
const hs5m = $derived(client ? client.dsps5 * 2 ** 32 : 0);
|
const hs5m = $derived(client ? client.dsps5 * 2 ** 32 : 0);
|
||||||
@@ -88,7 +94,7 @@
|
|||||||
title="View per-user stats"
|
title="View per-user stats"
|
||||||
>{btcAddress}</button>
|
>{btcAddress}</button>
|
||||||
{#if tls}
|
{#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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
|
|||||||
+18
-3
@@ -71,9 +71,10 @@ export type StratumClient = {
|
|||||||
workername: string;
|
workername: string;
|
||||||
userid: number;
|
userid: number;
|
||||||
// Index into ckpool's serverurl[] array; identifies which stratum
|
// Index into ckpool's serverurl[] array; identifies which stratum
|
||||||
// bind the client connected on. We use this to tag TLS clients:
|
// bind the client connected on. What each index *means* depends on
|
||||||
// index 0 is the public plaintext bind, index 1 is the loopback-only
|
// how the deployment rendered ckpool.conf, so resolve it through
|
||||||
// bind that stunnel forwards TLS traffic to.
|
// Snapshot.stratum_servers rather than hardcoding — see
|
||||||
|
// connectionOf() in format.ts.
|
||||||
server: number;
|
server: number;
|
||||||
bestdiff: number;
|
bestdiff: number;
|
||||||
};
|
};
|
||||||
@@ -112,6 +113,16 @@ export type HashratePoint = {
|
|||||||
v: number; // H/s
|
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 = {
|
export type Snapshot = {
|
||||||
generated_at: string;
|
generated_at: string;
|
||||||
pool: PoolStats | null;
|
pool: PoolStats | null;
|
||||||
@@ -137,6 +148,10 @@ export type Snapshot = {
|
|||||||
// Optional override for explorer links. Empty/undefined means the
|
// Optional override for explorer links. Empty/undefined means the
|
||||||
// UI falls back to its mempool.space defaults.
|
// UI falls back to its mempool.space defaults.
|
||||||
mempool_base_url?: string;
|
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"
|
// Submission attempt tracking — count of "Possible block solve"
|
||||||
// log lines vs "Solved and confirmed" lines. A growing gap means
|
// log lines vs "Solved and confirmed" lines. A growing gap means
|
||||||
// submissions are being rejected by bitcoind.
|
// submissions are being rejected by bitcoind.
|
||||||
|
|||||||
Reference in New Issue
Block a user