diff --git a/dashboard/lib/health.js b/dashboard/lib/health.js index 855ad94..72b4f8c 100644 --- a/dashboard/lib/health.js +++ b/dashboard/lib/health.js @@ -28,6 +28,14 @@ import { rateVerification } from './stats.js'; * drynet3. An hour means something is genuinely wrong, not slow. */ const PAYOUT_STALL_SEC = 3600; +/* The enforcer's first template after a new block carries only the witness + * commitment; the sidechain commitments follow on the next poll, ~30s later. + * Measured on betanet: every block, never longer than one 30s poll. A bare + * template younger than this is that gap, not a fault — four polls of grace, + * and an enforcer that really stopped committing still fails within one + * monitor interval after it. */ +const TEMPLATE_COMMIT_GRACE_SEC = 120; + /* The block subsidy at a height, on the standard schedule: 50 BTC, halved * every 210,000 blocks. Chains derived from Bitcoin — mainnet, testnet, * signet, and forknets that keep their parent's height — all share it; @@ -415,10 +423,14 @@ export function health(handle) { * so nothing else complains — but no sidechain can be merge-mined into * them, which is what stalled Thunder before. */ checks.push(guard('template_commitments', 'Templates carry sidechain commitments', () => { - const r = one(d, `SELECT source, cb_op_returns FROM templates - ORDER BY id DESC LIMIT 1`); + const r = one(d, `SELECT source, cb_op_returns, + strftime('%s','now') - ts AS age + FROM templates ORDER BY id DESC LIMIT 1`); if (!r) return { ok: true, unavailable: true, detail: 'no templates recorded yet' }; const ok = r.source === 'enforcer' && Number(r.cb_op_returns) > 1; + if (!ok && Number(r.age) < TEMPLATE_COMMIT_GRACE_SEC) + return { ok: true, value: Number(r.cb_op_returns), + detail: 'new block — commitments pending' }; return { ok, value: Number(r.cb_op_returns), detail: ok ? null : `mining ${r.source} templates with ${r.cb_op_returns} OP_RETURN(s) — no sidechain can merge-mine` }; diff --git a/dashboard/test/health.test.js b/dashboard/test/health.test.js index 8adc7a5..2b74c9b 100644 --- a/dashboard/test/health.test.js +++ b/dashboard/test/health.test.js @@ -293,6 +293,31 @@ test('an enforcer template with only a witness commitment is caught', () => { assert.ok(failing(health(db)).includes('template_commitments')); }); +/* Every new block opens with one bare enforcer template for ~30s before the + * commitments arrive. That gap must not raise the alarm. */ +test('a bare template only seconds old is the new-block gap, not a failure', () => { + const db = makeDb(); + db.prepare(`INSERT INTO templates (ts,height,prev_hash,bits,network_difficulty, + coinbase_value_sats,tx_count,tx_fees_sats,source,cb_spendable, + cb_op_returns,longpoll,rate_sats_per_diff) + VALUES (?, 3, 'bb', '1a', 111157.455, 312500000, 1, 0, + 'enforcer', 1, 1, 1, ?)`).run(Math.floor(Date.now() / 1000) - 20, RATE); + const h = health(db); + assert.ok(!failing(h).includes('template_commitments')); + assert.match(h.checks.find(c => c.id === 'template_commitments').detail, /pending/); +}); + +/* ...but one that outlives the grace is the real thing. */ +test('a bare template past the grace period is caught', () => { + const db = makeDb(); + db.prepare(`INSERT INTO templates (ts,height,prev_hash,bits,network_difficulty, + coinbase_value_sats,tx_count,tx_fees_sats,source,cb_spendable, + cb_op_returns,longpoll,rate_sats_per_diff) + VALUES (?, 3, 'bb', '1a', 111157.455, 312500000, 1, 0, + 'enforcer', 1, 1, 1, ?)`).run(Math.floor(Date.now() / 1000) - 180, RATE); + assert.ok(failing(health(db)).includes('template_commitments')); +}); + /* A check that cannot run must not read as a pass, and must not take the * page down either. */ test('a DB missing a table degrades to unavailable, not to healthy', () => {