Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions dashboard/lib/health.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,14 +368,27 @@ export function health(handle) {
* a template far behind the tip), not chain noise to be swallowed.
*
* Only settled candidates count toward the ratio: 'pending' means nothing
* has been able to verify it yet, which is not evidence either way. */
* has been able to verify it yet, which is not evidence either way.
*
* The unit is a HEIGHT, not a row. A template stuck on an old tip (an
* enforcer wedged in a mempool re-sync, say) keeps miners solving the same
* height, and every solve is its own rejected row: one incident on
* vaultnet left six rows at 974,037, which kept this check red for over a
* week after the fix while every block since had confirmed. A height
* counts as reached if any candidate there confirmed, and as lost if all
* of them were orphaned or rejected. A pool losing blocks at many heights
* still fails exactly as before. */
checks.push(guard('orphan_rate', 'Blocks reaching the chain', () => {
const r = one(d, `
SELECT COUNT(*) FILTER (WHERE status = 'confirmed') AS good,
COUNT(*) FILTER (WHERE status IN ('orphaned','rejected')) AS lost
FROM (SELECT status FROM blocks_found
WHERE status <> 'pending'
ORDER BY ts DESC LIMIT 100)`);
SELECT COUNT(*) FILTER (WHERE reached = 1) AS good,
COUNT(*) FILTER (WHERE reached = 0) AS lost
FROM (SELECT height,
MAX(status = 'confirmed') AS reached,
MAX(ts) AS last_ts
FROM blocks_found
WHERE status IN ('confirmed','orphaned','rejected')
GROUP BY height
ORDER BY last_ts DESC LIMIT 100)`);
const good = Number(r?.good || 0);
const lost = Number(r?.lost || 0);
const settled = good + lost;
Expand All @@ -388,7 +401,7 @@ export function health(handle) {
value: Math.round(pct),
detail: pct >= 50
? null
: `only ${good} of the last ${settled} candidates reached the chain`,
: `only ${good} of the last ${settled} heights the pool solved reached the chain`,
};
}));

Expand Down
42 changes: 42 additions & 0 deletions dashboard/test/health.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,48 @@ test('a pool whose candidates never reach the chain is caught', () => {
assert.ok(failing(h).includes('orphan_rate'));
});

/* The vaultnet incident, 2026-10-29: a wedged enforcer pinned the template at
* 974,037 and six solves there were rejected, one incident. Every block after
* the fix confirmed. Counted per row that is 4 of 10 and red for a week;
* counted per height it is 4 of 5. */
test('many rejected solves at one stuck height are one incident', () => {
const db = makeDb();
db.prepare('DELETE FROM blocks_found').run();
const ins = db.prepare(`INSERT INTO blocks_found (ts,height,hash,reward_sats,fee_sats,status)
VALUES (?, ?, ?, 0, 0, ?)`);
ins.run(1700000000, 974036, 'ok0', 'confirmed');
for (let i = 0; i < 6; i++) ins.run(1700000100 + i, 974037, `stuck${i}`, 'rejected');
ins.run(1700001000, 982926, 'ok1', 'confirmed');
ins.run(1700002000, 982965, 'ok2', 'confirmed');
ins.run(1700003000, 983139, 'ok3', 'confirmed');
const h = health(db);
assert.ok(!failing(h).includes('orphan_rate'));
});

/* A losing candidate at a height another candidate of ours won is not a lost
* height: the pool got that block. */
test('a height the pool won is reached despite a losing sibling', () => {
const db = makeDb();
db.prepare('DELETE FROM blocks_found').run();
const ins = db.prepare(`INSERT INTO blocks_found (ts,height,hash,reward_sats,fee_sats,status)
VALUES (?, ?, ?, 0, 0, ?)`);
ins.run(1700000000, 500, 'lose', 'orphaned');
ins.run(1700000001, 500, 'win', 'confirmed');
const h = health(db);
assert.ok(!failing(h).includes('orphan_rate'));
});

/* Grouping must not hide a pool that really is losing blocks: rejects at
* many distinct heights still outweigh the confirmed ones. */
test('lost blocks at many heights still fail', () => {
const db = makeDb();
const ins = db.prepare(`INSERT INTO blocks_found (ts,height,hash,reward_sats,fee_sats,status)
VALUES (?, ?, ?, 0, 0, 'rejected')`);
for (let i = 0; i < 3; i++) ins.run(1700000100 + i, 900000 + i, `rej${i}`);
const h = health(db);
assert.ok(failing(h).includes('orphan_rate'));
});

/* A pool that has found nothing yet has not failed at anything. */
test('no settled candidates is not an orphan-rate failure', () => {
const db = makeDb();
Expand Down
Loading