bwatch: poll chain and append blocks
What changed, and why it matters
This commit adds a routine background loop to a new Core Lightning plugin called bwatch. The plugin periodically asks Bitcoin for the latest block, downloads any missing blocks one at a time, keeps an in-memory history, and saves each block to the plugin's datastore. It is described by the author as the 'happy path' only; handling of chain reorganizations, watch notifications, and matching logic is explicitly left for later commits. There is no security-relevant behavior, vulnerability, or incident described in the commit or supplied references.
No security action required. This is a feature-introduction commit for a new plugin's normal block-sync loop. Reviewers may want to verify that the deferred reorg-detection and parent-hash validation commits are merged before this plugin is used in production, but that is outside the scope of this commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch implements the chain-polling loop in plugins/bwatch/bwatch.c and plugins/bwatch/bwatch.h. It schedules a timer (bwatch_poll_chain) that calls getchaininfo, compares the returned blockcount to bwatch->current_height, fetches the next block via getrawblockbyheight, parses the hex block with bitcoin_block_from_hex, updates current_height/current_blockhash, appends to an in-memory history via bwatch_add_block_to_history, and persists a wire-format block record to the datastore via bwatch_add_block_to_datastore. After a successful persist it reschedules the timer with zero delay to catch up back-to-back; once caught up it uses the configured poll interval (default 30s, tunable via –bwatch-poll-interval). On parse or RPC errors it logs and reschedules at the normal interval. The commit explicitly notes that reorg detection, parent-hash validation, watchman notifications, and watch matching are deferred to subsequent commits.
Changed components
plugins/bwatch/bwatch.cplugins/bwatch/bwatch.hInspect captured patch +197 / −0
diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c
index 3830a373..097375ed 100644
--- a/plugins/bwatch/bwatch.c
+++ b/plugins/bwatch/bwatch.c
@@ -1,5 +1,9 @@
#include "config.h"
#include <ccan/array_size/array_size.h>
+#include <ccan/ptrint/ptrint.h>
+#include <common/json_param.h>
+#include <common/json_parse.h>
+#include <common/json_stream.h>
#include <common/memleak.h>
#include <plugins/bwatch/bwatch.h>
#include <plugins/bwatch/bwatch_interface.h>
@@ -12,6 +16,186 @@ struct bwatch *bwatch_of(struct plugin *plugin)
return plugin_get_data(plugin, struct bwatch);
}
+/*
+ * ============================================================================
+ * BLOCK PROCESSING: Polling
+ *
+ * Each cycle: getchaininfo → if blockcount > current_height, fetch the next
+ * block via getrawblockbyheight, append it to the in-memory history, persist
+ * it, and reschedule the next poll once the datastore write completes.
+ *
+ * Reorg detection (parent-hash mismatch) and watch matching land in
+ * subsequent commits.
+ * ============================================================================
+ */
+
+static struct command_result *handle_block(struct command *cmd,
+ const char *method,
+ const char *buf,
+ const jsmntok_t *result,
+ ptrint_t *block_height);
+
+/* Parse the bitcoin block out of a getrawblockbyheight response. */
+static struct bitcoin_block *block_from_response(const char *buf,
+ const jsmntok_t *result,
+ struct bitcoin_blkid *blockhash_out)
+{
+ const jsmntok_t *blocktok = json_get_member(buf, result, "block");
+ struct bitcoin_block *block;
+
+ if (!blocktok)
+ return NULL;
+
+ block = bitcoin_block_from_hex(tmpctx, chainparams,
+ buf + blocktok->start,
+ blocktok->end - blocktok->start);
+ if (block && blockhash_out)
+ bitcoin_block_blkid(block, blockhash_out);
+
+ return block;
+}
+
+/* Fetch a block by height for normal polling. */
+static struct command_result *fetch_block_handle(struct command *cmd,
+ u32 height)
+{
+ struct out_req *req = jsonrpc_request_start(cmd, "getrawblockbyheight",
+ handle_block, handle_block,
+ int2ptr(height));
+ json_add_u32(req->js, "height", height);
+ return send_outreq(req);
+}
+
+/* Reschedule at the configured interval (used when there's nothing new to
+ * fetch, or on error). Once we're caught up to bitcoind's tip, this is
+ * what governs the steady-state poll cadence. */
+static struct command_result *poll_finished(struct command *cmd)
+{
+ struct bwatch *bwatch = bwatch_of(cmd->plugin);
+
+ bwatch->poll_timer = global_timer(cmd->plugin,
+ time_from_msec(bwatch->poll_interval_ms),
+ bwatch_poll_chain, NULL);
+ return timer_complete(cmd);
+}
+
+/* Just persisted a block — there may be more to catch up to, so poll again
+ * immediately rather than waiting for the full interval. Once getchaininfo
+ * reports no change, poll_finished resets us to the steady-state cadence. */
+static struct command_result *fetch_more(struct command *cmd)
+{
+ struct bwatch *bwatch = bwatch_of(cmd->plugin);
+
+ bwatch->poll_timer = global_timer(cmd->plugin, time_from_sec(0),
+ bwatch_poll_chain, NULL);
+ return timer_complete(cmd);
+}
+
+/* Process one block fetched from bitcoind: update tip, append to history,
+ * then persist; the poll is rescheduled once the datastore write completes. */
+static struct command_result *handle_block(struct command *cmd,
+ const char *method UNUSED,
+ const char *buf,
+ const jsmntok_t *result,
+ ptrint_t *block_height)
+{
+ struct bwatch *bwatch = bwatch_of(cmd->plugin);
+ struct bitcoin_blkid blockhash;
+ struct bitcoin_block *block;
+
+ block = block_from_response(buf, result, &blockhash);
+ if (!block) {
+ plugin_log(cmd->plugin, LOG_UNUSUAL,
+ "Failed to get/parse block %u: '%.*s'",
+ (unsigned int)ptr2int(block_height),
+ json_tok_full_len(result),
+ json_tok_full(buf, result));
+ return poll_finished(cmd);
+ }
+
+ bwatch->current_height = ptr2int(block_height);
+ bwatch->current_blockhash = blockhash;
+ bwatch_add_block_to_history(bwatch, bwatch->current_height, &blockhash,
+ &block->hdr.prev_hash);
+
+ struct block_record_wire br = {
+ bwatch->current_height,
+ bwatch->current_blockhash,
+ block->hdr.prev_hash,
+ };
+ return bwatch_add_block_to_datastore(cmd, &br, fetch_more);
+}
+
+/* getchaininfo response: pick the next block to fetch (or just reschedule). */
+static struct command_result *getchaininfo_done(struct command *cmd,
+ const char *method UNUSED,
+ const char *buf,
+ const jsmntok_t *result,
+ void *unused UNUSED)
+{
+ struct bwatch *bwatch = bwatch_of(cmd->plugin);
+ u32 blockheight;
+ const char *err;
+
+ err = json_scan(tmpctx, buf, result,
+ "{blockcount:%}",
+ JSON_SCAN(json_to_number, &blockheight));
+ if (err) {
+ plugin_log(cmd->plugin, LOG_BROKEN,
+ "getchaininfo parse failed: %s", err);
+ return poll_finished(cmd);
+ }
+
+ if (blockheight > bwatch->current_height) {
+ u32 target_height;
+
+ /* On first init we jump straight to the chain tip; afterwards
+ * we catch up one block at a time so handle_block can validate
+ * each parent hash (added in a later commit). */
+ if (bwatch->current_height == 0) {
+ plugin_log(cmd->plugin, LOG_DBG,
+ "First poll: init at block %u",
+ blockheight);
+ target_height = blockheight;
+ } else {
+ target_height = bwatch->current_height + 1;
+ }
+
+ return fetch_block_handle(cmd, target_height);
+ }
+
+ plugin_log(cmd->plugin, LOG_DBG,
+ "No block change, current_height remains %u",
+ bwatch->current_height);
+ return poll_finished(cmd);
+}
+
+/* Non-fatal: bcli may not have come up yet — log and retry on the next poll. */
+static struct command_result *getchaininfo_failed(struct command *cmd,
+ const char *method UNUSED,
+ const char *buf,
+ const jsmntok_t *result,
+ void *unused UNUSED)
+{
+ plugin_log(cmd->plugin, LOG_DBG,
+ "getchaininfo failed (bcli not ready?): %.*s",
+ json_tok_full_len(result), json_tok_full(buf, result));
+ return poll_finished(cmd);
+}
+
+struct command_result *bwatch_poll_chain(struct command *cmd,
+ void *unused UNUSED)
+{
+ struct bwatch *bwatch = bwatch_of(cmd->plugin);
+ struct out_req *req;
+
+ req = jsonrpc_request_start(cmd, "getchaininfo",
+ getchaininfo_done, getchaininfo_failed,
+ NULL);
+ json_add_u32(req->js, "last_height", bwatch->current_height);
+ return send_outreq(req);
+}
+
static const char *init(struct command *cmd,
const char *buf UNUSED,
const jsmntok_t *config UNUSED)
@@ -34,6 +218,9 @@ static const char *init(struct command *cmd,
bwatch_load_block_history(cmd, bwatch);
bwatch_load_watches_from_datastore(cmd, bwatch);
+ /* Kick off the chain-poll loop. */
+ bwatch->poll_timer = global_timer(cmd->plugin, time_from_sec(0),
+ bwatch_poll_chain, NULL);
return NULL;
}
diff --git a/plugins/bwatch/bwatch.h b/plugins/bwatch/bwatch.h
index 6a15ff80..7a128ef5 100644
--- a/plugins/bwatch/bwatch.h
+++ b/plugins/bwatch/bwatch.h
@@ -14,6 +14,9 @@ struct outpoint_watches;
struct scid_watches;
struct blockdepth_watches;
+/* Timer handle returned by global_timer; defined in libplugin. */
+struct plugin_timer;
+
/* 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. */
@@ -64,6 +67,8 @@ struct bwatch {
struct scid_watches *scid_watches;
struct blockdepth_watches *blockdepth_watches;
+ /* Active poll timer; rescheduled at the end of every poll cycle. */
+ struct plugin_timer *poll_timer;
u32 poll_interval_ms;
};
@@ -73,4 +78,9 @@ 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);
+/* Timer callback: kicks off one chain-poll cycle (getchaininfo →
+ * getrawblockbyheight → persist → reschedule). Exposed so other modules
+ * can schedule a poll from their own callbacks. */
+struct command_result *bwatch_poll_chain(struct command *cmd, void *unused);
+
#endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_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.