From f8c948c842c13a022815ed41e9261479ec7e4ccd Mon Sep 17 00:00:00 2001 From: rob Date: Mon, 5 Oct 2026 11:33:53 +0200 Subject: [PATCH] dashboard: give a new block's bare template a grace period MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The enforcer's first template after every block carries only the witness commitment; the sidechain commitments arrive on the next poll, ~30s later. template_commitments judged only the newest row, so a health run landing in that gap failed — and since the snapshot is held for 5 minutes, it outlasted the broadcaster's debounce and posted a "ledger health check is failing" / "pass again" pair a few times a day. Measured on betanet over 7 days: 183 bare templates, one per block, none older than 30s. A bare template younger than 120s now reads as "commitments pending"; one that outlives it still fails, within one monitor interval. Co-Authored-By: Claude Opus 5.5 (1M context) --- dashboard/lib/health.js | 16 ++++++++++++++-- dashboard/test/health.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) 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', () => {