Skip to content

Commit fc64cfd

Browse files
committed
Miner-facing audit on /worker/:name (no auth needed)
Miners now get the same per-worker audit the operator sees on /admin/worker/:id, scoped to just their own row on the existing public per-worker page. No new URL, no auth — the URL already identifies the worker, and the numbers are their own data. New cards on /worker/<name> when the pool is in a PPS mode (skipped in solo mode; the top card falls back to 'N/A' for owed): - PPS balance: owed, accrued (all time), paid (all time), last credited. - Audit — why is this number what it is?: accepted shares, Σ difficulty, rate (POOL_PPS_SATS_PER_DIFF), Σ FLOOR(diff × rate) labelled 'authoritative', stored pps_credits.accrued_sats, match indicator (✓ or ⚠). - Guidance to the operator's admin view for the per-share running_accrued trail without exposing that page to the world. lib/stats.js: worker() takes a new 4th arg (rate), and if a pps_credits row exists for the worker, returns pps_audit with the same shape the admin view uses. The extra 4th arg keeps existing callers (dashboard/... older tests?) working via arguments[3] lookup. server.js: refactors PPS_SATS_PER_DIFF up to the top so both the public /worker/:name and the admin routes share it (was previously declared in the admin patch block only, TDZ error waiting to happen once we referenced it from the /worker route). Smoke-tested locally: dashboard on synthetic DB returns 200, both cards render, ✓/⚠ indicator fires correctly against a mismatch.
1 parent b920797 commit fc64cfd

3 files changed

Lines changed: 111 additions & 6 deletions

File tree

dashboard/lib/stats.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,36 @@ export function worker(handle, name, windowSec = 86400) {
242242
});
243243
}
244244

245+
/* Self-service PPS audit — same cross-check the admin view runs,
246+
* but scoped to this one worker. Returns null in solo mode (no row
247+
* in pps_credits). `rate` is passed in by the caller (from env). */
248+
let ppsAudit = null;
249+
const credit = d.prepare(`
250+
SELECT accrued_sats, paid_sats, last_updated
251+
FROM pps_credits WHERE worker_id = ?
252+
`).get(w.id);
253+
if (credit) {
254+
const rate = Number(arguments[3] || 0);
255+
const totals = d.prepare(`
256+
SELECT COUNT(*) AS share_count,
257+
COALESCE(SUM(difficulty), 0) AS sum_difficulty,
258+
COALESCE(SUM(CAST(difficulty * ? AS INTEGER)), 0) AS accrued_computed
259+
FROM shares
260+
WHERE worker_id = ?
261+
`).get(rate, w.id);
262+
const accrued = Number(credit.accrued_sats || 0);
263+
const paid = Number(credit.paid_sats || 0);
264+
ppsAudit = {
265+
rate,
266+
accrued, paid, owed: accrued - paid,
267+
last_updated: Number(credit.last_updated || 0),
268+
share_count: Number(totals.share_count),
269+
sum_difficulty: Number(totals.sum_difficulty),
270+
accrued_computed: Number(totals.accrued_computed),
271+
matches: Number(totals.accrued_computed) === accrued,
272+
};
273+
}
274+
245275
return {
246276
worker: {
247277
name: w.name,
@@ -254,6 +284,7 @@ export function worker(handle, name, windowSec = 86400) {
254284
shares,
255285
buckets,
256286
window_sec: windowSec,
287+
pps_audit: ppsAudit,
257288
};
258289
}
259290

dashboard/server.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const db = openDb(path.resolve(__dirname, DB_PATH));
2626
/* Shown on the public "Connect a miner" card. Env-driven so the shipped
2727
* template doesn't hardcode any particular deployment's host. */
2828
const PUBLIC_STRATUM_URL = process.env.PUBLIC_STRATUM_URL || 'stratum+tcp://<pool-host>:3334';
29+
/* Read once here so the public per-worker page can render the audit
30+
* cross-check for the miner without needing admin auth. The admin
31+
* patch block below also reads this — the constant is shared. */
32+
const PPS_SATS_PER_DIFF = parseFloat(process.env.POOL_PPS_SATS_PER_DIFF || '1000');
2933

3034
app.get('/', (req, res) => {
3135
const ov = stats.overview(db);
@@ -42,7 +46,7 @@ app.get('/', (req, res) => {
4246
});
4347

4448
app.get('/worker/:name', (req, res) => {
45-
const w = stats.worker(db, req.params.name);
49+
const w = stats.worker(db, req.params.name, 86400, PPS_SATS_PER_DIFF);
4650
if (!w.worker) return res.status(404).render('404', { what: 'worker' });
4751
res.render('worker', { ...w, name: req.params.name, fmtHashrate: stats.fmtHashrate });
4852
});
@@ -81,10 +85,8 @@ const ADMIN_USER = process.env.ADMIN_USER || '';
8185
const ADMIN_PASS = process.env.ADMIN_PASSWORD || '';
8286
const RESERVE_ADDRESS = process.env.POOL_THUNDER_RESERVE_ADDRESS || '(unset)';
8387
const THUNDER_RPC_URL = process.env.THUNDER_RPC_URL || 'http://127.0.0.1:6009';
84-
/* The rate the C proxy uses to convert difficulty → sats. Must match
85-
* proxy.conf's pps_sats_per_diff or the worker-audit cross-check goes
86-
* red. Passed via the same env-driven config path as the admin creds. */
87-
const PPS_SATS_PER_DIFF = parseFloat(process.env.POOL_PPS_SATS_PER_DIFF || '1000');
88+
/* PPS_SATS_PER_DIFF is declared at the top of the file so both the
89+
* public /worker/:name view and the admin routes can share it. */
8890

8991
function requireAdminAuth(req, res, next) {
9092
if (!ADMIN_USER || !ADMIN_PASS) {

dashboard/views/worker.ejs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ if (n > 1) {
3939
<span class="tag">worker</span>
4040
</header>
4141
<main class="wrap">
42+
<%
43+
const fmtSats = (sats) => {
44+
if (!sats) return '0 sats';
45+
const btc = sats / 1e8;
46+
if (btc >= 0.01) return fmtN(sats) + ' sats (' + btc.toFixed(8) + ' BTC)';
47+
return fmtN(sats) + ' sats';
48+
};
49+
%>
4250
<section class="card">
4351
<h2><%= worker.name %></h2>
4452
<% if (worker.payout_address) { %>
@@ -49,10 +57,74 @@ if (n > 1) {
4957
<div><label>Shares (24h)</label><strong><%= fmtN(worker.window_shares) %></strong></div>
5058
<div><label>First seen</label><strong title="<%= fmtTs(worker.first_seen) %>"><%= ago(worker.first_seen) %></strong></div>
5159
<div><label>Last seen</label><strong title="<%= fmtTs(worker.last_seen) %>"><%= ago(worker.last_seen) %></strong></div>
52-
<div><label>Balance</label><strong class="muted">N/A</strong></div>
60+
<div><label>Owed</label><strong><%= pps_audit ? fmtSats(pps_audit.owed) : 'N/A (solo mode)' %></strong></div>
5361
</div>
5462
</section>
5563

64+
<% if (pps_audit) { %>
65+
<section class="card">
66+
<h2>PPS balance</h2>
67+
<div class="grid">
68+
<div><label>Owed (waiting for payout)</label><strong><%= fmtSats(pps_audit.owed) %></strong></div>
69+
<div><label>Accrued (all time)</label><strong><%= fmtSats(pps_audit.accrued) %></strong></div>
70+
<div><label>Paid (all time)</label><strong><%= fmtSats(pps_audit.paid) %></strong></div>
71+
<div><label>Last credited</label><strong title="<%= fmtTs(pps_audit.last_updated) %>"><%= ago(pps_audit.last_updated) %></strong></div>
72+
</div>
73+
<p class="muted small" style="margin-top:0.75em">
74+
Payouts flow to your Thunder address on a cadence once the
75+
pool's Thunder reserve is funded. Ask the operator if
76+
you've been "Owed" a substantial balance for a long time
77+
and it isn't dropping.
78+
</p>
79+
</section>
80+
81+
<section class="card">
82+
<h2>Audit — why is this number what it is?</h2>
83+
<p class="muted small" style="margin-top:0">
84+
Every accepted share credits your worker
85+
<code>FLOOR(share_difficulty × <%= fmtN(pps_audit.rate) %>)</code>
86+
sats. The rate is <code>pps_sats_per_diff</code> from the
87+
pool's config; the difficulty is what the pool held you at
88+
when the share landed (vardiff adjusts it based on how
89+
fast you're finding hashes). Everything below is
90+
derivable from the raw <code>shares</code> table — the
91+
stored balance is a running total the pool maintains, but
92+
you can always recompute it from scratch.
93+
</p>
94+
<div class="grid">
95+
<div><label>Accepted shares</label><strong><%= fmtN(pps_audit.share_count) %></strong></div>
96+
<div><label>Σ difficulty</label><strong><%= pps_audit.sum_difficulty.toFixed(6) %></strong></div>
97+
<div><label>Rate (sats × diff)</label><strong><%= fmtN(pps_audit.rate) %></strong></div>
98+
<div><label>Σ FLOOR(diff × rate)<br><span class="muted small">the authoritative sum</span></label><strong><%= fmtSats(pps_audit.accrued_computed) %></strong></div>
99+
<div><label>Stored accrued<br><span class="muted small">from pps_credits</span></label><strong><%= fmtSats(pps_audit.accrued) %></strong></div>
100+
<div><label>Match?</label>
101+
<% if (pps_audit.matches) { %>
102+
<strong style="color:#5b8">✓ Yes — every share accounted for</strong>
103+
<% } else { %>
104+
<strong style="color:#c66">⚠ Off by <%= fmtSats(Math.abs(pps_audit.accrued - pps_audit.accrued_computed)) %></strong>
105+
<% } %>
106+
</div>
107+
</div>
108+
<% if (!pps_audit.matches) { %>
109+
<p class="muted small" style="color:#c66;margin-top:0.75em">
110+
A ⚠ here usually means the pool's config
111+
(<code>pps_sats_per_diff</code>) changed at some
112+
point after this worker started, so recomputing at
113+
the current rate doesn't reproduce the historical
114+
total. Ask the operator to confirm the rate history.
115+
Your stored balance is authoritative for payouts either
116+
way.
117+
</p>
118+
<% } %>
119+
<p class="muted small" style="margin-top:0.75em">
120+
For an even deeper trail (per-share running total, daily
121+
breakdown), the operator has an admin-only page at
122+
<code>/admin/worker/&lt;id&gt;</code>. Same numbers — just
123+
without exposing other miners' data on this public page.
124+
</p>
125+
</section>
126+
<% } %>
127+
56128
<section class="card">
57129
<h2>Hashrate · 10-min buckets · last 24h</h2>
58130
<% if (n > 1) { %>

0 commit comments

Comments
 (0)