lightningd: instantiate watchman at startup
What changed, and why it matters
This commit changes when Core Lightning starts its internal 'watchman' component, which tracks blockchain events. Previously the watchman was created later; now it is created earlier at startup so it can queue requests for the bwatch plugin and replay them once that plugin is ready. The commit also wraps database loading in a transaction and replaces a callback hook with an explicit notification function. No subsystems actually register watches yet, so this is preparatory infrastructure work rather than a live security fix.
No immediate security action required. Treat as normal infrastructure refactoring. Review the follow-up wallet migration PR that actually registers watches to assess whether queued operations are replayed safely and whether the transaction boundaries are correct under crash recovery.
Security signals we found
Startup ordering change for a security-relevant subsystem (blockchain watchman)
Database transaction wrapping added around pending-ops/tip loading
Callback indirection replaced with explicit notification function
No vulnerability description or CVE reference present in commit
Evidence from the diff
The patch moves watchman_new() invocation to immediately after setup_topology() in lightningd.c, and adds watchman_notify_plugin_ready() calls from plugin.c when any plugin reaches INIT_COMPLETE. It removes the on_plugin_ready callback pointer on the plugins struct in favor of an explicit exported function, and wraps load_pending_ops() and load_tip() inside a database transaction. The change is described by the author as enabling queued bwatch RPC requests and replay of pending operations once the plugin is ready. No actual watch registration subsystems are wired in yet.
Changed components
lightningd/lightningd.clightningd/plugin.clightningd/watchman.clightningd/watchman.hlightningd/test/run-find_my_abspath.cInspect captured patch +22 / −6
diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c
index 3e95e59c..6810baee 100644
--- a/lightningd/lightningd.c
+++ b/lightningd/lightningd.c
@@ -75,6 +75,7 @@
#include <lightningd/plugin_hook.h>
#include <lightningd/runes.h>
#include <lightningd/subd.h>
+#include <lightningd/watchman.h>
#include <sys/resource.h>
#include <wallet/invoices.h>
#include <wally_bip32.h>
@@ -1347,6 +1348,10 @@ int main(int argc, char *argv[])
setup_topology(ld->topology);
trace_span_end(ld->topology);
+ /*~ Stand up the watchman: it queues bwatch RPC requests until the
+ * bwatch plugin reports ready, then replays them. */
+ ld->watchman = watchman_new(ld, ld);
+
db_begin_transaction(ld->wallet->db);
trace_span_start("delete_old_htlcs", ld->wallet);
wallet_delete_old_htlcs(ld->wallet);
diff --git a/lightningd/plugin.c b/lightningd/plugin.c
index 444c626e..9edb27b0 100644
--- a/lightningd/plugin.c
+++ b/lightningd/plugin.c
@@ -29,6 +29,7 @@
#include <lightningd/plugin_control.h>
#include <lightningd/plugin_hook.h>
#include <lightningd/subd.h>
+#include <lightningd/watchman.h>
/* Only this file can include this generated header! */
# include <plugins/list_of_builtin_plugins_gen.h>
@@ -2087,6 +2088,7 @@ static void plugin_config_cb(const char *buffer,
}
if (tal_count(plugin->custom_msgs))
tell_connectd_custommsgs(plugin->plugins);
+ watchman_notify_plugin_ready(plugin->plugins->ld, plugin);
notify_plugin_started(plugin->plugins->ld, plugin);
check_plugins_initted(plugin->plugins);
}
diff --git a/lightningd/test/run-find_my_abspath.c b/lightningd/test/run-find_my_abspath.c
index 3d227bd9..fc378229 100644
--- a/lightningd/test/run-find_my_abspath.c
+++ b/lightningd/test/run-find_my_abspath.c
@@ -222,6 +222,9 @@ struct wallet *wallet_new(struct lightningd *ld UNNEEDED, struct timers *timers
/* Generated stub for wallet_sanity_check */
bool wallet_sanity_check(struct wallet *w UNNEEDED)
{ fprintf(stderr, "wallet_sanity_check called!\n"); abort(); }
+/* Generated stub for watchman_new */
+struct watchman *watchman_new(const tal_t *ctx UNNEEDED, struct lightningd *ld UNNEEDED)
+{ fprintf(stderr, "watchman_new called!\n"); abort(); }
/* AUTOGENERATED MOCKS END */
struct logger *crashlog;
diff --git a/lightningd/watchman.c b/lightningd/watchman.c
index a70e0c44..e5ebaf46 100644
--- a/lightningd/watchman.c
+++ b/lightningd/watchman.c
@@ -135,8 +135,6 @@ static void load_pending_ops(struct watchman *wm)
}
}
-static void watchman_on_plugin_ready(struct lightningd *ld, struct plugin *plugin);
-
/* Apply --rescan: negative means absolute height (only go back),
* positive means relative (go back N blocks from stored tip). */
static void apply_rescan(struct watchman *wm, struct lightningd *ld)
@@ -167,16 +165,15 @@ struct watchman *watchman_new(const tal_t *ctx, struct lightningd *ld)
wm->ld = ld;
wm->pending_ops = tal_arr(wm, struct pending_op *, 0);
+ db_begin_transaction(ld->wallet->db);
load_pending_ops(wm);
load_tip(wm);
+ db_commit_transaction(ld->wallet->db);
apply_rescan(wm, ld);
log_info(ld->log, "Watchman: height=%u, %zu pending ops",
wm->last_processed_height, tal_count(wm->pending_ops));
- /* Replay pending ops exactly when bwatch transitions to INIT_COMPLETE. */
- ld->plugins->on_plugin_ready = watchman_on_plugin_ready;
-
return wm;
}
@@ -359,7 +356,7 @@ void watchman_replay_pending(struct lightningd *ld)
/* Replay pending ops when bwatch is ready. On a fresh node current_height
* is still 0, so we defer to json_block_processed where it's guaranteed > 0. */
-static void watchman_on_plugin_ready(struct lightningd *ld, struct plugin *plugin)
+void watchman_notify_plugin_ready(struct lightningd *ld, struct plugin *plugin)
{
struct watchman *wm = ld->watchman;
diff --git a/lightningd/watchman.h b/lightningd/watchman.h
index d5221f2e..8271c23a 100644
--- a/lightningd/watchman.h
+++ b/lightningd/watchman.h
@@ -7,6 +7,7 @@
struct lightningd;
struct pending_op;
+struct plugin;
struct short_channel_id;
/* lightningd's view of bwatch. bwatch lives in a separate process and tells
@@ -58,6 +59,14 @@ typedef void (*depth_found_fn)(struct lightningd *ld,
u32 depth,
u32 blockheight);
+/**
+ * watchman_notify_plugin_ready - Called by plugin.c when any plugin reaches INIT_COMPLETE
+ *
+ * Checks whether the newly-ready plugin is bwatch and, if so, replays any
+ * pending watch operations that were queued before bwatch was available.
+ */
+void watchman_notify_plugin_ready(struct lightningd *ld, struct plugin *plugin);
+
/**
* watchman_new - Create and initialize a new watchman instance
* @ctx: tal context to allocate from
Why this scored 18/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.