What changed, and why it matters
This commit adds a new feature to the bwatch plugin that remembers recent Bitcoin block history in the node's internal database. It is a reliability improvement for handling blockchain reorganizations, not a security fix. There is no indication of a vulnerability being patched.
No security action required. Review as normal feature code for correctness and robustness of reorg handling.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces persistent storage of block records (height, hash, previous hash) for the bwatch plugin. It adds startup replay of stored history, async datastore writes with ‘must-create’ semantics, and helper functions for adding/deleting/loading block records. The change is additive and feature-oriented; no security bug is addressed in the diff.
Changed components
plugins/bwatch/bwatch.cplugins/bwatch/bwatch.hplugins/bwatch/bwatch_store.cplugins/bwatch/bwatch_store.hInspect captured patch +186 / −0
diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c
index 9ab46eed..3c6ceb6c 100644
--- a/plugins/bwatch/bwatch.c
+++ b/plugins/bwatch/bwatch.c
@@ -5,6 +5,7 @@
#include <plugins/bwatch/bwatch_interface.h>
#include <plugins/bwatch/bwatch_scanner.h>
#include <plugins/bwatch/bwatch_store.h>
+#include <plugins/bwatch/bwatch_wiregen.h>
struct bwatch *bwatch_of(struct plugin *plugin)
{
@@ -24,6 +25,14 @@ static const char *init(struct command *cmd,
bwatch->scid_watches = new_htable(bwatch, scid_watches);
bwatch->blockdepth_watches = new_htable(bwatch, blockdepth_watches);
+ bwatch->block_history = tal_arr(bwatch, struct block_record_wire, 0);
+
+ /* Replay persisted block history. load_block_history sets
+ * current_height / current_blockhash from the most recent record;
+ * if there are no records, fall back to zero so the first poll
+ * initialises us at the chain tip. */
+ bwatch_load_block_history(cmd, bwatch);
+
return NULL;
}
diff --git a/plugins/bwatch/bwatch.h b/plugins/bwatch/bwatch.h
index 1052ddc3..6a15ff80 100644
--- a/plugins/bwatch/bwatch.h
+++ b/plugins/bwatch/bwatch.h
@@ -2,6 +2,7 @@
#define LIGHTNING_PLUGINS_BWATCH_BWATCH_H
#include "config.h"
+#include <bitcoin/block.h>
#include <bitcoin/short_channel_id.h>
#include <bitcoin/tx.h>
#include <plugins/libplugin.h>
@@ -13,6 +14,11 @@ struct outpoint_watches;
struct scid_watches;
struct blockdepth_watches;
+/* Wire-format block record stored in lightningd's datastore.
+ * Defined by bwatch_wiregen.h; forward-declared here to avoid pulling
+ * the generated header into every consumer of bwatch.h. */
+struct block_record_wire;
+
/* Watch type discriminator. */
enum watch_type {
WATCH_SCRIPTPUBKEY,
@@ -47,6 +53,11 @@ struct watch {
* confirm-height) without dispatching on type at every call site. */
struct bwatch {
struct plugin *plugin;
+ u32 current_height;
+ struct bitcoin_blkid current_blockhash;
+ /* Oldest first, most recent last. Used to replay a reorg by
+ * peeling tips off until the parent hash matches the new chain. */
+ struct block_record_wire *block_history;
struct scriptpubkey_watches *scriptpubkey_watches;
struct outpoint_watches *outpoint_watches;
@@ -56,6 +67,9 @@ struct bwatch {
u32 poll_interval_ms;
};
+/* Helper: get last block_history (or NULL) */
+const struct block_record_wire *bwatch_last_block(const struct bwatch *bwatch);
+
/* Helper: retrieve the bwatch state from a plugin handle. */
struct bwatch *bwatch_of(struct plugin *plugin);
diff --git a/plugins/bwatch/bwatch_store.c b/plugins/bwatch/bwatch_store.c
index 68ba5baa..6ae177c4 100644
--- a/plugins/bwatch/bwatch_store.c
+++ b/plugins/bwatch/bwatch_store.c
@@ -1,7 +1,14 @@
#include "config.h"
#include <ccan/crypto/siphash24/siphash24.h>
+#include <ccan/json_out/json_out.h>
#include <ccan/mem/mem.h>
+#include <ccan/str/hex/hex.h>
+#include <ccan/tal/str/str.h>
+#include <common/json_param.h>
+#include <common/json_parse.h>
+#include <common/mkdatastorekey.h>
#include <plugins/bwatch/bwatch_store.h>
+#include <plugins/bwatch/bwatch_wiregen.h>
const struct scriptpubkey *scriptpubkey_watch_keyof(const struct watch *w)
{
@@ -148,3 +155,146 @@ void bwatch_remove_watch_from_hash(struct bwatch *bwatch, struct watch *w)
}
abort();
}
+
+/* List all datastore entries under a key prefix (up to 2 components).
+ * Shared between block_history loading and (in a follow-up commit)
+ * watch loading. */
+static const jsmntok_t *bwatch_list_datastore(const tal_t *ctx,
+ struct command *cmd,
+ const char *key1, const char *key2,
+ const char **buf_out)
+{
+ struct json_out *params = json_out_new(tmpctx);
+ const jsmntok_t *result;
+
+ json_out_start(params, NULL, '{');
+ json_out_start(params, "key", '[');
+ json_out_addstr(params, NULL, key1);
+ if (key2)
+ json_out_addstr(params, NULL, key2);
+ json_out_end(params, ']');
+ json_out_end(params, '}');
+
+ result = jsonrpc_request_sync(ctx, cmd, "listdatastore", params, buf_out);
+ return json_get_member(*buf_out, result, "datastore");
+}
+
+/* Datastore write completed (success or expected failure such as duplicate).
+ * Either way, invoke the caller's continuation to keep the poll chain alive. */
+static struct command_result *block_store_done(struct command *cmd,
+ const char *method UNNEEDED,
+ const char *buf UNNEEDED,
+ const jsmntok_t *result UNNEEDED,
+ struct command_result *(*done)(struct command *))
+{
+ return done(cmd);
+}
+
+struct command_result *bwatch_add_block_to_datastore(
+ struct command *cmd,
+ const struct block_record_wire *br,
+ struct command_result *(*done)(struct command *cmd))
+{
+ /* Zero-pad to 10 digits so listdatastore returns blocks in height
+ * order ("0000000100" < "0000000101"). */
+ const char **key = mkdatastorekey(tmpctx, "bwatch", "block_history",
+ take(tal_fmt(NULL, "%010u", br->height)));
+ const u8 *data = towire_bwatch_block(tmpctx, br);
+
+ plugin_log(cmd->plugin, LOG_DBG, "Added block %u to datastore", br->height);
+
+ /* Chain `done` as both success and failure continuation so the poll
+ * cmd is held alive until the write is acknowledged. Write failure
+ * (e.g. duplicate on restart) is non-fatal — the poll must continue. */
+ return jsonrpc_set_datastore_binary(cmd, key,
+ data, tal_bytelen(data),
+ "must-create",
+ block_store_done, block_store_done,
+ done);
+}
+
+void bwatch_add_block_to_history(struct bwatch *bwatch, u32 height,
+ const struct bitcoin_blkid *hash,
+ const struct bitcoin_blkid *prev_hash)
+{
+ struct block_record_wire br;
+
+ br.height = height;
+ br.hash = *hash;
+ br.prev_hash = *prev_hash;
+ tal_arr_expand(&bwatch->block_history, br);
+
+ plugin_log(bwatch->plugin, LOG_DBG,
+ "Added block %u to history (now %zu blocks)",
+ height, tal_count(bwatch->block_history));
+}
+
+void bwatch_delete_block_from_datastore(struct command *cmd, u32 height)
+{
+ struct json_out *params = json_out_new(tmpctx);
+ const char *buf;
+
+ json_out_start(params, NULL, '{');
+ json_out_start(params, "key", '[');
+ json_out_addstr(params, NULL, "bwatch");
+ json_out_addstr(params, NULL, "block_history");
+ json_out_addstr(params, NULL, tal_fmt(tmpctx, "%010u", height));
+ json_out_end(params, ']');
+ json_out_end(params, '}');
+
+ jsonrpc_request_sync(tmpctx, cmd, "deldatastore", params, &buf);
+
+ plugin_log(cmd->plugin, LOG_DBG, "Deleted block %u from datastore", height);
+}
+
+const struct block_record_wire *bwatch_last_block(const struct bwatch *bwatch)
+{
+ if (tal_count(bwatch->block_history) == 0)
+ return NULL;
+
+ return &bwatch->block_history[tal_count(bwatch->block_history) - 1];
+}
+
+void bwatch_load_block_history(struct command *cmd, struct bwatch *bwatch)
+{
+ const char *buf;
+ const jsmntok_t *datastore, *t;
+ size_t i;
+ const struct block_record_wire *most_recent;
+
+ datastore = bwatch_list_datastore(tmpctx, cmd, "bwatch", "block_history", &buf);
+
+ json_for_each_arr(i, t, datastore) {
+ const u8 *data = json_tok_bin_from_hex(tmpctx, buf,
+ json_get_member(buf, t, "hex"));
+ struct block_record_wire br;
+
+ if (!data)
+ plugin_err(cmd->plugin,
+ "Bad block_history hex %.*s",
+ json_tok_full_len(t),
+ json_tok_full(buf, t));
+
+ if (!fromwire_bwatch_block(data, &br)) {
+ plugin_err(cmd->plugin,
+ "Bad block_history %.*s",
+ json_tok_full_len(t),
+ json_tok_full(buf, t));
+ }
+ tal_arr_expand(&bwatch->block_history, br);
+ }
+
+ most_recent = bwatch_last_block(bwatch);
+ if (most_recent) {
+ bwatch->current_height = most_recent->height;
+ bwatch->current_blockhash = most_recent->hash;
+ plugin_log(cmd->plugin, LOG_DBG,
+ "Restored %zu blocks from datastore, current height=%u",
+ tal_count(bwatch->block_history),
+ bwatch->current_height);
+ } else {
+ bwatch->current_height = 0;
+ memset(&bwatch->current_blockhash, 0,
+ sizeof(bwatch->current_blockhash));
+ }
+}
diff --git a/plugins/bwatch/bwatch_store.h b/plugins/bwatch/bwatch_store.h
index 4ca60049..b3e4804f 100644
--- a/plugins/bwatch/bwatch_store.h
+++ b/plugins/bwatch/bwatch_store.h
@@ -59,4 +59,17 @@ struct watch *bwatch_get_watch(struct bwatch *bwatch,
const u32 *confirm_height);
void bwatch_remove_watch_from_hash(struct bwatch *bwatch, struct watch *w);
+/* Block storage: in-memory history mirrors what's persisted under
+ * ["bwatch", "block_history", "%010u"]. Writes are async; reads happen
+ * once at startup. */
+struct command_result *bwatch_add_block_to_datastore(
+ struct command *cmd,
+ const struct block_record_wire *br,
+ struct command_result *(*done)(struct command *cmd));
+void bwatch_add_block_to_history(struct bwatch *bwatch, u32 height,
+ const struct bitcoin_blkid *hash,
+ const struct bitcoin_blkid *prev_hash);
+void bwatch_delete_block_from_datastore(struct command *cmd, u32 height);
+void bwatch_load_block_history(struct command *cmd, struct bwatch *bwatch);
+
#endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_STORE_H */
Why this scored 15/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.