lightningd: add watchman storage and persistence skeleton
What changed, and why it matters
This commit adds the first pieces of a new 'watchman' subsystem in Core Lightning. It creates helper functions to save and load binary data (like block hashes) from the wallet database, reads the last processed blockchain height/hash at startup, and applies the existing --rescan option to move the starting height backward if requested. The watchman object is declared in the main daemon structure but is not yet created or used anywhere. There is no user-facing functionality or active network behavior in this commit.
No security action required. Treat as normal feature scaffolding. Reviewers may want to verify in follow-up commits that apply_rescan cannot be tricked into an upward rescan and that db_get_blobvar blob length is validated before memcpy, but those concerns are outside the scope of this skeleton commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces db_set_blobvar/db_get_blobvar for BLOB storage in the vars table, load_tip() to restore last_processed_height and last_processed_hash, apply_rescan() to honor –rescan by lowering the loaded tip, and watchman_new() to allocate and initialize the watchman. A forward declaration and struct field are added to struct lightningd, but no call site instantiates watchman_new yet. The code is scaffolding only: no watchers are registered, no plugin messages are sent, and no consensus-critical logic is exercised.
Changed components
lightningd/watchman.cdb/exec.cdb/exec.hlightningd/lightningd.hlightningd/test/run-invoice-select-inchan.cInspect captured patch +108 / −8
diff --git a/db/exec.c b/db/exec.c
index 382ad9f1..68c2ced5 100644
--- a/db/exec.c
+++ b/db/exec.c
@@ -89,6 +89,39 @@ s64 db_get_intvar(struct db *db, const char *varname, s64 defval)
return res;
}
+void db_set_blobvar(struct db *db, const char *varname, const u8 *val, size_t len)
+{
+ size_t changes;
+ struct db_stmt *stmt = db_prepare_v2(db, SQL("UPDATE vars SET blobval=? WHERE name=?;"));
+ db_bind_blob(stmt, val, len);
+ db_bind_text(stmt, varname);
+ db_exec_prepared_v2(stmt);
+ changes = db_count_changes(stmt);
+ tal_free(stmt);
+
+ if (changes == 0) {
+ stmt = db_prepare_v2(db, SQL("INSERT INTO vars (name, blobval) VALUES (?, ?);"));
+ db_bind_text(stmt, varname);
+ db_bind_blob(stmt, val, len);
+ db_exec_prepared_v2(stmt);
+ tal_free(stmt);
+ }
+}
+
+const u8 *db_get_blobvar(const tal_t *ctx, struct db *db, const char *varname)
+{
+ struct db_stmt *stmt = db_prepare_v2(
+ db, SQL("SELECT blobval FROM vars WHERE name=? LIMIT 1"));
+ db_bind_text(stmt, varname);
+
+ const u8 *res = NULL;
+ if (db_query_prepared_canfail(stmt) && db_step(stmt))
+ res = db_col_arr(ctx, stmt, "blobval", u8);
+
+ tal_free(stmt);
+ return res;
+}
+
/* Leak tracking. */
/* By making the update conditional on the current value we expect we
diff --git a/db/exec.h b/db/exec.h
index c852d950..242b2f52 100644
--- a/db/exec.h
+++ b/db/exec.h
@@ -4,6 +4,7 @@
#include <ccan/short_types/short_types.h>
#include <ccan/take/take.h>
+#include <ccan/tal/tal.h>
struct db;
@@ -23,6 +24,10 @@ void db_set_intvar(struct db *db, const char *varname, s64 val);
*/
s64 db_get_intvar(struct db *db, const char *varname, s64 defval);
+void db_set_blobvar(struct db *db, const char *varname, const u8 *val, size_t len);
+/* Returns a tal-allocated blob, or NULL if not found. */
+const u8 *db_get_blobvar(const tal_t *ctx, struct db *db, const char *varname);
+
/* Get the current data version (entries). */
u32 db_data_version_get(struct db *db);
diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h
index 81a03d41..6d778c92 100644
--- a/lightningd/lightningd.h
+++ b/lightningd/lightningd.h
@@ -12,6 +12,7 @@
#include <wallet/wallet.h>
struct amount_msat;
+struct watchman;
/* Various adjustable things. */
struct config {
@@ -244,6 +245,7 @@ struct lightningd {
/* Derive all our BIP86 keys from here */
struct ext_key *bip86_base;
struct wallet *wallet;
+ struct watchman *watchman;
/* Outstanding waitsendpay commands. */
struct list_head waitsendpay_commands;
diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c
index 0357cf94..d41bf546 100644
--- a/lightningd/test/run-invoice-select-inchan.c
+++ b/lightningd/test/run-invoice-select-inchan.c
@@ -158,12 +158,6 @@ bool command_deprecated_in_ok(struct command *cmd UNNEEDED,
const char *depr_start UNNEEDED,
const char *depr_end UNNEEDED)
{ fprintf(stderr, "command_deprecated_in_ok called!\n"); abort(); }
-/* Generated stub for command_deprecated_out_ok */
-bool command_deprecated_out_ok(struct command *cmd UNNEEDED,
- const char *fieldname UNNEEDED,
- const char *depr_start UNNEEDED,
- const char *depr_end UNNEEDED)
-{ fprintf(stderr, "command_deprecated_out_ok called!\n"); abort(); }
/* Generated stub for command_dev_apis */
bool command_dev_apis(const struct command *cmd UNNEEDED)
{ fprintf(stderr, "command_dev_apis called!\n"); abort(); }
diff --git a/lightningd/watchman.c b/lightningd/watchman.c
index 9a1ed50a..60abf812 100644
--- a/lightningd/watchman.c
+++ b/lightningd/watchman.c
@@ -1,5 +1,71 @@
#include "config.h"
+#include <db/exec.h>
+#include <lightningd/lightningd.h>
+#include <lightningd/log.h>
#include <lightningd/watchman.h>
+#include <wallet/wallet.h>
-/* Definitions land in subsequent commits (storage skeleton, watchman_new,
- * send_to_bwatch, ack handling, enqueue/replay). */
+/*
+ * Watchman is the lightningd-side counterpart to the bwatch plugin.
+ * It tracks how far we've processed the chain (last_processed_height +
+ * hash, persisted in the SQL `vars` table), queues outbound watch ops
+ * while bwatch is starting up, and dispatches watch_found / watch_revert /
+ * blockdepth notifications to subdaemon-specific handlers.
+ *
+ * This commit lands just enough machinery to construct a watchman and
+ * recover the persisted tip; the pending-op queue and ack lifecycle land
+ * in subsequent commits.
+ */
+
+static void load_tip(struct watchman *wm)
+{
+ struct db *db = wm->ld->wallet->db;
+ const u8 *blob;
+
+ wm->last_processed_height = db_get_intvar(db, "last_watchman_block_height", 0);
+
+ blob = db_get_blobvar(tmpctx, db, "last_watchman_block_hash");
+ if (blob) {
+ assert(tal_bytelen(blob) == sizeof(struct bitcoin_blkid));
+ memcpy(&wm->last_processed_hash, blob, sizeof(wm->last_processed_hash));
+ }
+}
+
+/* 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)
+{
+ u32 stored = wm->last_processed_height;
+ u32 target;
+
+ if (ld->config.rescan < 0)
+ target = (u32)(-ld->config.rescan); /* absolute height */
+ else if (stored > (u32)ld->config.rescan)
+ target = stored - (u32)ld->config.rescan; /* go back N blocks */
+ else
+ target = 0; /* rescan exceeds stored height, start from genesis */
+
+ /* Only adjust downward; upward targets are validated later in chaininfo */
+ if (target < stored) {
+ log_debug(ld->log,
+ "Rescanning: adjusting watchman height from %u to %u",
+ stored, target);
+ wm->last_processed_height = target;
+ }
+}
+
+struct watchman *watchman_new(const tal_t *ctx, struct lightningd *ld)
+{
+ struct watchman *wm = talz(ctx, struct watchman);
+
+ wm->ld = ld;
+ wm->pending_ops = tal_arr(wm, struct pending_op *, 0);
+
+ load_tip(wm);
+ apply_rescan(wm, ld);
+
+ log_info(ld->log, "Watchman: height=%u, %zu pending ops",
+ wm->last_processed_height, tal_count(wm->pending_ops));
+
+ return wm;
+}
Why this scored 12/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.