From 8e243dfd3e2cb719e253eb75285d1ad84f2af853 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:34:57 +0000 Subject: [PATCH] Observer: one checkpoint_locked event per index (the poll claims the state before its first await; the FinalityLock notification path raced it and the live feed showed two locked lines 30 ms apart); a lock claimed by the notification gets its votes_seen from the next poll Co-Authored-By: Claude Fable 5.1 --- tools/observer/observer.mjs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/observer/observer.mjs b/tools/observer/observer.mjs index 569be0f79..abe90c9da 100644 --- a/tools/observer/observer.mjs +++ b/tools/observer/observer.mjs @@ -512,7 +512,8 @@ async function tick(rpc) { } // ---------- Finality v2 (spec 03) ---------- -const checkpointStates = new Map(); // index -> last state written +const checkpointStates = new Map(); // index -> last state written; set BEFORE any await, so the two lock paths never both record +const checkpointDetailed = new Set(); // indices written with votesSeen (the FinalityLock notification carries none; the poll fills it in once) let finalitySupported = null; // null = unknown, false = the node has no finality RPC let lastWeightsAt = 0; let lastWeights = null; @@ -554,16 +555,22 @@ async function finalityTick(rpc) { if (!certBackfillDone) backfillCertificate(rpc, cps).catch(e => log('certificate backfill failed', e.message)); for (const cp of cps.checkpoints || []) { const prev = checkpointStates.get(cp.index); - if (prev !== cp.state || prev === undefined) { - await upsertCheckpoint(cp); - if (cp.state === 'locked' && prev !== 'locked') await recordLock(cp); + const changed = prev !== cp.state || prev === undefined; + const detail = cp.votesSeen !== undefined && !checkpointDetailed.has(cp.index); + if (changed || detail) { + // claim the transition before the first await: the FinalityLock notification handler checks this map synchronously, + // and until 4 October 2026 both paths recorded the same lock (two "locked" events 30 ms apart on the live feed) checkpointStates.set(cp.index, cp.state); + if (cp.votesSeen !== undefined) checkpointDetailed.add(cp.index); + await upsertCheckpoint(cp); + if (changed && cp.state === 'locked' && prev !== 'locked') await recordLock(cp); } else if (cp.state !== 'locked' && (cp.index % 1 === 0)) { // vote counts move while a checkpoint is open: refresh it await upsertCheckpoint(cp); } } for (const k of checkpointStates.keys()) if (k + 500 < Number(cps.nextIndex)) checkpointStates.delete(k); + for (const k of checkpointDetailed) if (k + 500 < Number(cps.nextIndex)) checkpointDetailed.delete(k); const now = Date.now(); if (now - lastWeightsAt > 10_000) { lastWeightsAt = now;