bwatch: notify watch owners on reorg
What changed, and why it matters
This commit fixes a bug in Core Lightning's blockchain-watch plugin (bwatch). When the Bitcoin chain reorganizes (a block that was considered part of the chain gets replaced by a different one), bwatch was removing the old tip block without telling the rest of the node. The fix makes bwatch notify all relevant 'watch owners' before the block is discarded, so lightningd can undo any state changes tied to that now-orphaned block. Without the fix, the node could keep stale or incorrect state after a reorg, which in a Lightning node can lead to incorrect channel balances or missed on-chain events.
Treat this as a correctness/security fix and include it in the next maintenance release. Review lightningd-side watch_revert handlers to confirm they are idempotent and safely handle concurrent reorg events. Consider adding regression tests that simulate a reorg and verify that each watch type receives the expected watch_revert notification.
Security signals we found
Missing reorg notification could leave lightningd state inconsistent after chain reorganization
Revert handlers are now invoked in the same order blocks are removed
Owner snapshotting prevents mutation-safety issues during callback dispatch
Conditional notification by start_block avoids spurious revert traffic for older watches
Evidence from the diff
The patch adds bwatch_notify_reorg_watches() in plugins/bwatch/bwatch.c. It is called inside bwatch_remove_tip() before the block is deleted from the datastore. The function iterates over scriptpubkey, outpoint, short-channel-id (scid), and blockdepth watches, snapshots their owners, and dispatches watch_revert notifications. Scriptpubkey watches (no anchor block) notify all owners; outpoint/scid/blockdepth watches notify only those whose start_block >= removed_height. Owner snapshotting prevents use-after-free or iterator invalidation if a revert handler calls watchman_unwatch_* and mutates the tables. The change is purely additive and defensive.
Changed components
plugins/bwatch/bwatch.cbwatch_remove_tip()watch_revert dispatch pathlightningd reorg/undo handlersInspect captured patch +61 / −0
diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c
index cd64aedd..4e548608 100644
--- a/plugins/bwatch/bwatch.c
+++ b/plugins/bwatch/bwatch.c
@@ -79,6 +79,63 @@ static struct command_result *poll_finished(struct command *cmd)
return timer_complete(cmd);
}
+/* Send watch_revert for every owner affected by losing @removed_height. */
+static void bwatch_notify_reorg_watches(struct command *cmd,
+ struct bwatch *bwatch,
+ u32 removed_height)
+{
+ const char **owners = tal_arr(tmpctx, const char *, 0);
+ struct watch *w;
+
+ /* Snapshot owners first; revert handlers may call watchman_del and
+ * mutate these tables. */
+
+ /* Scriptpubkey watches are perennial: always notify. */
+ struct scriptpubkey_watches_iter sit;
+ for (w = scriptpubkey_watches_first(bwatch->scriptpubkey_watches, &sit);
+ w;
+ w = scriptpubkey_watches_next(bwatch->scriptpubkey_watches, &sit)) {
+ for (size_t i = 0; i < tal_count(w->owners); i++)
+ tal_arr_expand(&owners, w->owners[i]);
+ }
+
+ /* Outpoint/scid/blockdepth: only notify watches whose anchor block is
+ * being torn down (start_block >= removed_height). Older long-lived
+ * watches stay armed and will refire naturally on the new chain. */
+ struct outpoint_watches_iter oit;
+ for (w = outpoint_watches_first(bwatch->outpoint_watches, &oit);
+ w;
+ w = outpoint_watches_next(bwatch->outpoint_watches, &oit)) {
+ if (w->start_block < removed_height)
+ continue;
+ for (size_t i = 0; i < tal_count(w->owners); i++)
+ tal_arr_expand(&owners, w->owners[i]);
+ }
+
+ struct scid_watches_iter scit;
+ for (w = scid_watches_first(bwatch->scid_watches, &scit);
+ w;
+ w = scid_watches_next(bwatch->scid_watches, &scit)) {
+ if (w->start_block < removed_height)
+ continue;
+ for (size_t i = 0; i < tal_count(w->owners); i++)
+ tal_arr_expand(&owners, w->owners[i]);
+ }
+
+ struct blockdepth_watches_iter bdit;
+ for (w = blockdepth_watches_first(bwatch->blockdepth_watches, &bdit);
+ w;
+ w = blockdepth_watches_next(bwatch->blockdepth_watches, &bdit)) {
+ if (w->start_block < removed_height)
+ continue;
+ for (size_t i = 0; i < tal_count(w->owners); i++)
+ tal_arr_expand(&owners, w->owners[i]);
+ }
+
+ for (size_t i = 0; i < tal_count(owners); i++)
+ bwatch_send_watch_revert(cmd, owners[i], removed_height);
+}
+
/* Remove tip block on reorg */
void bwatch_remove_tip(struct command *cmd, struct bwatch *bwatch)
{
@@ -95,6 +152,10 @@ void bwatch_remove_tip(struct command *cmd, struct bwatch *bwatch)
bwatch->current_height,
fmt_bitcoin_blkid(tmpctx, &bwatch->current_blockhash));
+ /* Notify owners of any watch affected by losing this block before we
+ * tear it down, so they can roll back in the same order things happened. */
+ bwatch_notify_reorg_watches(cmd, bwatch, bwatch->current_height);
+
/* Delete block from datastore */
bwatch_delete_block_from_datastore(cmd, bwatch->current_height);
Why this scored 63/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.