lightningd: only run watchman under --experimental-bwatch
What changed, and why it matters
This commit gates a new experimental chain-watching subsystem called 'bwatch' behind an explicit --experimental-bwatch flag. Previously, the watchman helper was always created, which could have caused both the old chain watcher and the new bwatch watcher to run at the same time and race against each other. The change makes the watchman a no-op unless the user explicitly opts in, so ordinary users continue using the legacy path only.
Treat this as a hardening/race-prevention change rather than an active vulnerability. If running pre-patch builds with bwatch present, verify that --experimental-bwatch was not enabled by default and that no dual-watcher races occurred. No emergency action is indicated by the diff alone.
Security signals we found
Race condition between legacy chain watcher and new bwatch watcher prevented by gating watchman creation behind an opt-in flag
watchman_* entry points made NULL-safe so disabled code paths are explicit no-ops
JSON RPC command now returns a controlled error instead of dereferencing a NULL watchman
Experimental feature explicitly marked as opt-in via --experimental-bwatch
Evidence from the diff
The patch modifies lightningd.c to only instantiate ld->watchman when the user has supplied –experimental-bwatch, detected by scanning parsed configvars because the flag is registered by the bwatch plugin rather than lightningd itself. watchman.c is updated so that watchman_add, watchman_del, watchman_ack, and watchman_replay_pending return early when ld->watchman is NULL. A JSON chaininfo command now fails with ‘Watchman not initialized’ if the watchman is absent. Tests are adjusted to opt in explicitly instead of enabling bwatch globally.
Changed components
lightningd/lightningd.clightningd/watchman.cbwatch plugin integrationchain_topology chain watcherwallet transaction and UTXO trackingInspect captured patch +52 / −5
diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c
index 6810baee..682900ce 100644
--- a/lightningd/lightningd.c
+++ b/lightningd/lightningd.c
@@ -46,6 +46,7 @@
/*~ This is common code: routines shared by one or more executables
* (separate daemons, or the lightning-cli program). */
+#include <common/configvar.h>
#include <common/daemon.h>
#include <common/deprecation.h>
#include <common/ecdh_hsmd.h>
@@ -288,6 +289,11 @@ static struct lightningd *new_lightningd(const tal_t *ctx)
* so set it to NULL explicitly now. */
ld->wallet = NULL;
+ /*~ Only created if the user opts into --experimental-bwatch, but
+ * plugin startup (watchman_notify_plugin_ready) examines it, so set
+ * it to NULL explicitly now. */
+ ld->watchman = NULL;
+
/*~ Behavioral options */
ld->accept_extra_tlv_types = tal_arr(ld, u64, 0);
@@ -1164,6 +1170,18 @@ static void setup_fd_limit(struct lightningd *ld, size_t num_channels)
}
}
+/*~ Has the user opted into the experimental bwatch chain watcher? The
+ * --experimental-bwatch flag is registered by the bwatch plugin, not by
+ * lightningd, so we look for it in the parsed configvars rather than
+ * keeping our own copy. */
+static bool bwatch_enabled(const struct lightningd *ld)
+{
+ const char **names = tal_arr(tmpctx, const char *, 1);
+
+ names[0] = "experimental-bwatch";
+ return configvar_first(ld->configvars, names) != NULL;
+}
+
int main(int argc, char *argv[])
{
struct lightningd *ld;
@@ -1349,8 +1367,16 @@ int main(int argc, char *argv[])
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);
+ * bwatch plugin reports ready, then replays them. Must come after
+ * setup_topology so start_block reflects the last-processed height.
+ *
+ * bwatch is opt-in for now: the --experimental-bwatch flag is
+ * registered by the bwatch plugin, so peek at the configvar to gate
+ * the lightningd side too. Without it, ld->watchman stays NULL and
+ * the watchman_* entry points are no-ops, leaving chain_topology as
+ * the only chain watcher. */
+ if (bwatch_enabled(ld))
+ ld->watchman = watchman_new(ld, ld);
db_begin_transaction(ld->wallet->db);
trace_span_start("delete_old_htlcs", ld->wallet);
diff --git a/lightningd/watchman.c b/lightningd/watchman.c
index 14d98541..15dd6b32 100644
--- a/lightningd/watchman.c
+++ b/lightningd/watchman.c
@@ -286,7 +286,13 @@ static void watchman_add(struct lightningd *ld, const char *method,
const char *owner, const char *json_params)
{
struct watchman *wm = ld->watchman;
- char *op_id = tal_fmt(tmpctx, "%s:%s", method, owner);
+ char *op_id;
+
+ /* No-op unless --experimental-bwatch stood up the watchman. */
+ if (!wm)
+ return;
+
+ op_id = tal_fmt(tmpctx, "%s:%s", method, owner);
/* Remove any existing add for this owner */
watchman_ack(ld, op_id);
@@ -304,12 +310,18 @@ static void watchman_del(struct lightningd *ld, const char *method,
const char *owner, const char *json_params)
{
struct watchman *wm = ld->watchman;
- char *op_id = tal_fmt(tmpctx, "%s:%s", method, owner);
+ char *op_id, *add_op_id;
+
+ /* No-op unless --experimental-bwatch stood up the watchman. */
+ if (!wm)
+ return;
+
+ op_id = tal_fmt(tmpctx, "%s:%s", method, owner);
/* Cancel any pending add for this owner. All del-methods are named
* "del<suffix>" and their paired add-method is "add<suffix>" */
assert(strstarts(method, "del"));
- char *add_op_id = tal_fmt(tmpctx, "add%s:%s", method + strlen("del"), owner);
+ add_op_id = tal_fmt(tmpctx, "add%s:%s", method + strlen("del"), owner);
watchman_ack(ld, add_op_id);
enqueue_op(wm, method, op_id, json_params);
}
@@ -326,6 +338,9 @@ void watchman_ack(struct lightningd *ld, const char *op_id)
{
struct watchman *wm = ld->watchman;
+ if (!wm)
+ return;
+
for (size_t i = 0; i < tal_count(wm->pending_ops); i++) {
if (streq(wm->pending_ops[i]->op_id, op_id)) {
db_remove(wm, op_id);
@@ -346,6 +361,9 @@ void watchman_replay_pending(struct lightningd *ld)
{
struct watchman *wm = ld->watchman;
+ if (!wm)
+ return;
+
for (size_t i = 0; i < tal_count(wm->pending_ops); i++) {
struct pending_op *op = wm->pending_ops[i];
send_to_bwatch(wm, method_from_op_id(tmpctx, op->op_id),
@@ -805,6 +823,9 @@ static struct command_result *json_chaininfo(struct command *cmd,
NULL))
return command_param_failed();
+ if (!cmd->ld->watchman)
+ return command_fail(cmd, LIGHTNINGD, "Watchman not initialized");
+
if (!streq(chain, chainparams->bip70_name))
fatal("Wrong network! Our Bitcoin backend is running on '%s',"
" but we expect '%s'.", chain, chainparams->bip70_name);
Why this scored 33/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.