From 60b0b6bcaec8bc0ba6111f904eb811a9415eaf5a Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 23 Apr 2026 13:29:30 +0930 Subject: [PATCH 01/77] common: add helpers for bitcoin blockids. Like bitcoin_txid, they are special backwards-printed snowflakes. Thanks Obama! Signed-off-by: Rusty Russell --- bitcoin/block.c | 22 +++++++++++++++------- bitcoin/block.h | 5 +++++ bitcoin/test/run-bitcoin_block_from_hex.c | 9 --------- common/json_parse.c | 7 +++++++ common/json_parse.h | 4 ++++ common/json_stream.c | 9 +++++++++ common/json_stream.h | 5 +++++ 7 files changed, 45 insertions(+), 16 deletions(-) diff --git a/bitcoin/block.c b/bitcoin/block.c index 5a36a7dfc890..6838f2c39344 100644 --- a/bitcoin/block.c +++ b/bitcoin/block.c @@ -228,16 +228,24 @@ void bitcoin_block_blkid(const struct bitcoin_block *b, *out = b->hdr.hash; } -static bool bitcoin_blkid_to_hex(const struct bitcoin_blkid *blockid, - char *hexstr, size_t hexstr_len) +bool bitcoin_blkid_from_hex(const char *hexstr, size_t hexstr_len, + struct bitcoin_blkid *blkid) { - struct bitcoin_txid fake_txid; - fake_txid.shad = blockid->shad; - return bitcoin_txid_to_hex(&fake_txid, hexstr, hexstr_len); + if (!hex_decode(hexstr, hexstr_len, blkid, sizeof(*blkid))) + return false; + reverse_bytes(blkid->shad.sha.u.u8, sizeof(blkid->shad.sha.u.u8)); + return true; } -char *fmt_bitcoin_blkid(const tal_t *ctx, - const struct bitcoin_blkid *blkid) +bool bitcoin_blkid_to_hex(const struct bitcoin_blkid *blkid, + char *hexstr, size_t hexstr_len) +{ + struct sha256_double rev = blkid->shad; + reverse_bytes(rev.sha.u.u8, sizeof(rev.sha.u.u8)); + return hex_encode(&rev, sizeof(rev), hexstr, hexstr_len); +} + +char *fmt_bitcoin_blkid(const tal_t *ctx, const struct bitcoin_blkid *blkid) { char *hexstr = tal_arr(ctx, char, hex_str_size(sizeof(*blkid))); diff --git a/bitcoin/block.h b/bitcoin/block.h index a3289ee7cc14..8a945ca46af9 100644 --- a/bitcoin/block.h +++ b/bitcoin/block.h @@ -52,6 +52,11 @@ void fromwire_chainparams(const u8 **cursor, size_t *max, const struct chainparams **chainparams); void towire_chainparams(u8 **cursor, const struct chainparams *chainparams); +bool bitcoin_blkid_from_hex(const char *hexstr, size_t hexstr_len, + struct bitcoin_blkid *blkid); +bool bitcoin_blkid_to_hex(const struct bitcoin_blkid *blkid, + char *hexstr, size_t hexstr_len); + char *fmt_bitcoin_blkid(const tal_t *ctx, const struct bitcoin_blkid *blkid); diff --git a/bitcoin/test/run-bitcoin_block_from_hex.c b/bitcoin/test/run-bitcoin_block_from_hex.c index 0dc0bd640fc8..94b26ad5d05f 100644 --- a/bitcoin/test/run-bitcoin_block_from_hex.c +++ b/bitcoin/test/run-bitcoin_block_from_hex.c @@ -62,15 +62,6 @@ static const char block[] = STRUCTEQ_DEF(sha256_double, 0, sha); -static bool bitcoin_blkid_from_hex(const char *hexstr, size_t hexstr_len, - struct bitcoin_blkid *blockid) -{ - struct bitcoin_txid fake_txid; - if (!bitcoin_txid_from_hex(hexstr, hexstr_len, &fake_txid)) - return false; - blockid->shad = fake_txid.shad; - return true; -} int main(int argc, const char *argv[]) { struct bitcoin_blkid prev; diff --git a/common/json_parse.c b/common/json_parse.c index 7841e0827ff9..5c1c68310b58 100644 --- a/common/json_parse.c +++ b/common/json_parse.c @@ -601,6 +601,13 @@ bool json_to_txid(const char *buffer, const jsmntok_t *tok, tok->end - tok->start, txid); } +bool json_to_bitcoin_blkid(const char *buffer, const jsmntok_t *tok, + struct bitcoin_blkid *blkid) +{ + return bitcoin_blkid_from_hex(buffer + tok->start, + tok->end - tok->start, blkid); +} + bool json_to_outpoint(const char *buffer, const jsmntok_t *tok, struct bitcoin_outpoint *op) { diff --git a/common/json_parse.h b/common/json_parse.h index 4706c3775f30..4c739154c4d9 100644 --- a/common/json_parse.h +++ b/common/json_parse.h @@ -108,6 +108,10 @@ bool json_to_msat(const char *buffer, const jsmntok_t *tok, bool json_to_txid(const char *buffer, const jsmntok_t *tok, struct bitcoin_txid *txid); +/* Extract a bitcoin blkid from this */ +bool json_to_bitcoin_blkid(const char *buffer, const jsmntok_t *tok, + struct bitcoin_blkid *blkid); + /* Extract a bitcoin outpoint from this */ bool json_to_outpoint(const char *buffer, const jsmntok_t *tok, struct bitcoin_outpoint *op); diff --git a/common/json_stream.c b/common/json_stream.c index 6a0746074584..3daefe469541 100644 --- a/common/json_stream.c +++ b/common/json_stream.c @@ -455,6 +455,15 @@ void json_add_txid(struct json_stream *result, const char *fieldname, json_add_string(result, fieldname, hex); } +void json_add_bitcoin_blkid(struct json_stream *result, const char *fieldname, + const struct bitcoin_blkid *blkid) +{ + char hex[hex_str_size(sizeof(*blkid))]; + + bitcoin_blkid_to_hex(blkid, hex, sizeof(hex)); + json_add_string(result, fieldname, hex); +} + void json_add_outpoint(struct json_stream *result, const char *fieldname, const struct bitcoin_outpoint *out) { diff --git a/common/json_stream.h b/common/json_stream.h index 7756c013d98f..3263dfd96d66 100644 --- a/common/json_stream.h +++ b/common/json_stream.h @@ -31,6 +31,7 @@ struct short_channel_id; struct sha256; struct preimage; struct bitcoin_tx; +struct bitcoin_blkid; struct wally_psbt; struct lease_rates; struct wireaddr; @@ -310,6 +311,10 @@ void json_add_channel_id(struct json_stream *response, void json_add_txid(struct json_stream *result, const char *fieldname, const struct bitcoin_txid *txid); +/* '"fieldname" : ' or "" if fieldname is NULL */ +void json_add_bitcoin_blkid(struct json_stream *result, const char *fieldname, + const struct bitcoin_blkid *blkid); + /* '"fieldname" : "txid:n" */ void json_add_outpoint(struct json_stream *result, const char *fieldname, const struct bitcoin_outpoint *out); From 64344d74034412f20dbdd18ce00b2abba5e5fe04 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 23 Apr 2026 13:29:30 +0930 Subject: [PATCH 02/77] common/json_stream: use json_out_addstrn for better efficiency. Signed-off-by: Rusty Russell --- common/json_stream.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/common/json_stream.c b/common/json_stream.c index 3daefe469541..d1ec9d41c771 100644 --- a/common/json_stream.c +++ b/common/json_stream.c @@ -193,13 +193,22 @@ void json_add_primitive(struct json_stream *js, tal_free_if_taken(val); } +void json_add_stringn(struct json_stream *js, + const char *fieldname, + const char *str TAKES, + size_t len) +{ + if (json_filter_ok(js->filter, fieldname)) + json_out_addstrn(js->jout, fieldname, str, len); + if (taken(str)) + tal_free(str); +} + void json_add_string(struct json_stream *js, const char *fieldname, const char *str TAKES) { - if (json_filter_ok(js->filter, fieldname)) - json_out_addstr(js->jout, fieldname, str); - tal_free_if_taken(str); + json_add_stringn(js, fieldname, str, strlen(str)); } static char *json_member_direct(struct json_stream *js, @@ -298,13 +307,6 @@ void json_add_s32(struct json_stream *result, const char *fieldname, json_add_primitive_fmt(result, fieldname, "%d", value); } -void json_add_stringn(struct json_stream *result, const char *fieldname, - const char *value TAKES, size_t value_len) -{ - json_add_str_fmt(result, fieldname, "%.*s", (int)value_len, value); - tal_free_if_taken(value); -} - void json_add_bool(struct json_stream *result, const char *fieldname, bool value) { json_add_primitive(result, fieldname, value ? "true" : "false"); From c92e3f742ca49fa529eb324b4fc1e13744e347d0 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 23 Apr 2026 13:29:30 +0930 Subject: [PATCH 03/77] libplugin: expose json_add_keypath. Signed-off-by: Rusty Russell --- plugins/libplugin.c | 6 +++--- plugins/libplugin.h | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/libplugin.c b/plugins/libplugin.c index b39058d46559..e681eac9d9c9 100644 --- a/plugins/libplugin.c +++ b/plugins/libplugin.c @@ -848,9 +848,9 @@ void rpc_scan(struct command *cmd, guide, method, err); } -static void json_add_keypath(struct json_out *jout, - const char *fieldname, - const char **keys) +void json_add_keypath(struct json_out *jout, + const char *fieldname, + const char **keys) { json_out_start(jout, fieldname, '['); for (size_t i = 0; i < tal_count(keys); i++) diff --git a/plugins/libplugin.h b/plugins/libplugin.h index bddba28f7735..68051e965916 100644 --- a/plugins/libplugin.h +++ b/plugins/libplugin.h @@ -696,6 +696,11 @@ struct listpeers_channel **json_to_listpeers_channels(const tal_t *ctx, const char *buffer, const jsmntok_t *tok); +/* Helper to write keys[] array (mainly for datastore ops) */ +void json_add_keypath(struct json_out *jout, + const char *fieldname, + const char **keys); + struct createonion_response { u8 *onion; struct secret *shared_secrets; From f6bc95f00a99f2ea01d15466e833cf38d3687b26 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri <101164840+sangbida@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:29:30 +0930 Subject: [PATCH 04/77] common: expose json_hex_to_be32/be64 These helper functions decode hex strings from JSON into big-endian 32-bit and 64-bit values, useful for parsing datastore entries exposing these into a more common space so they can be used by bwatch in the future. --- common/json_parse_simple.c | 13 +++++++++++++ common/json_parse_simple.h | 7 +++++++ plugins/bkpr/blockheights.c | 7 ------- plugins/bkpr/bookkeeper.c | 8 +------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/common/json_parse_simple.c b/common/json_parse_simple.c index 348be9ef6976..fbb2b12c67b5 100644 --- a/common/json_parse_simple.c +++ b/common/json_parse_simple.c @@ -2,6 +2,7 @@ #include "config.h" #include #include +#include #include #include #include @@ -151,6 +152,18 @@ bool json_to_bool(const char *buffer, const jsmntok_t *tok, bool *b) return false; } +bool json_hex_to_be32(const char *buffer, const jsmntok_t *tok, be32 *val) +{ + return hex_decode(buffer + tok->start, tok->end - tok->start, + val, sizeof(*val)); +} + +bool json_hex_to_be64(const char *buffer, const jsmntok_t *tok, be64 *val) +{ + return hex_decode(buffer + tok->start, tok->end - tok->start, + val, sizeof(*val)); +} + bool json_tok_is_num(const char *buffer, const jsmntok_t *tok) { diff --git a/common/json_parse_simple.h b/common/json_parse_simple.h index 0882812d76d4..56f97e2c14a1 100644 --- a/common/json_parse_simple.h +++ b/common/json_parse_simple.h @@ -2,6 +2,7 @@ #ifndef LIGHTNING_COMMON_JSON_PARSE_SIMPLE_H #define LIGHTNING_COMMON_JSON_PARSE_SIMPLE_H #include "config.h" +#include #include #include @@ -51,6 +52,12 @@ bool json_to_double(const char *buffer, const jsmntok_t *tok, double *num); /* Extract boolean from this */ bool json_to_bool(const char *buffer, const jsmntok_t *tok, bool *b); +/* Extract big-endian 32-bit from hex string (for datastore) */ +bool json_hex_to_be32(const char *buffer, const jsmntok_t *tok, be32 *val); + +/* Extract big-endian 64-bit from hex string (for datastore) */ +bool json_hex_to_be64(const char *buffer, const jsmntok_t *tok, be64 *val); + /* Is this a number? [0..9]+ */ bool json_tok_is_num(const char *buffer, const jsmntok_t *tok); diff --git a/plugins/bkpr/blockheights.c b/plugins/bkpr/blockheights.c index 35aa229e45df..5da70721353e 100644 --- a/plugins/bkpr/blockheights.c +++ b/plugins/bkpr/blockheights.c @@ -98,13 +98,6 @@ u32 find_blockheight(const struct bkpr *bkpr, return e ? e->height : 0; } -static bool json_hex_to_be32(const char *buffer, const jsmntok_t *tok, - be32 *val) -{ - return hex_decode(buffer + tok->start, tok->end - tok->start, - val, sizeof(*val)); -} - struct blockheights *init_blockheights(const tal_t *ctx, struct command *init_cmd) { diff --git a/plugins/bkpr/bookkeeper.c b/plugins/bkpr/bookkeeper.c index 41c6e34fab81..8ca5f967599d 100644 --- a/plugins/bkpr/bookkeeper.c +++ b/plugins/bkpr/bookkeeper.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -1831,13 +1832,6 @@ static const struct plugin_command commands[] = { }, }; -static bool json_hex_to_be64(const char *buffer, const jsmntok_t *tok, - be64 *val) -{ - return hex_decode(buffer + tok->start, tok->end - tok->start, - val, sizeof(*val)); -} - static void memleak_scan_currencyrates(struct htable *memtable, currencymap_t *currency_rates) { From d2f261fb3feda760fe2762c8f09f1664d7317503 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 23 Apr 2026 13:29:30 +0930 Subject: [PATCH 05/77] common: extract param_string_array from xpay into common. Signed-off-by: Rusty Russell --- common/json_param.c | 16 ++++++++++++++++ common/json_param.h | 5 +++++ plugins/xpay/xpay.c | 16 ---------------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/common/json_param.c b/common/json_param.c index 1b529707016f..18bd284c0f22 100644 --- a/common/json_param.c +++ b/common/json_param.c @@ -478,6 +478,22 @@ struct command_result *param_string_or_array(struct command *cmd, const char *na return param_string(cmd, name, buffer, tok, &(*result)->str); } +struct command_result *param_string_array(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + const char ***arr) +{ + size_t i; + const jsmntok_t *s; + + if (tok->type != JSMN_ARRAY) + return command_fail_badparam(cmd, name, buffer, tok, + "should be an array"); + *arr = tal_arr(cmd, const char *, tok->size); + json_for_each_arr(i, s, tok) + (*arr)[i] = json_strdup(*arr, buffer, s); + return NULL; +} + struct command_result *param_invstring(struct command *cmd, const char *name, const char * buffer, const jsmntok_t *tok, const char **str) diff --git a/common/json_param.h b/common/json_param.h index a000c71b1179..54d2fa4236b5 100644 --- a/common/json_param.h +++ b/common/json_param.h @@ -206,6 +206,11 @@ struct command_result *param_string_or_array(struct command *cmd, const char *na const char * buffer, const jsmntok_t *tok, struct str_or_arr **result); +/* Array of strings */ +struct command_result *param_string_array(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + const char ***arr); + /* Extract an invoice string from a generic string, strip the `lightning:` * prefix from it if needed. */ struct command_result *param_invstring(struct command *cmd, const char *name, diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c index e05e2df3f85e..61c62bab6c60 100644 --- a/plugins/xpay/xpay.c +++ b/plugins/xpay/xpay.c @@ -1706,22 +1706,6 @@ static struct command_result *populate_private_layer(struct command *cmd, return batch_done(aux_cmd, batch); } -static struct command_result *param_string_array(struct command *cmd, const char *name, - const char *buffer, const jsmntok_t *tok, - const char ***arr) -{ - size_t i; - const jsmntok_t *s; - - if (tok->type != JSMN_ARRAY) - return command_fail_badparam(cmd, name, buffer, tok, - "should be an array"); - *arr = tal_arr(cmd, const char *, tok->size); - json_for_each_arr(i, s, tok) - (*arr)[i] = json_strdup(*arr, buffer, s); - return NULL; -} - static struct command_result * preapproveinvoice_succeed(struct command *cmd, const char *method, From d6cb52eaa01f74c6c87d5bc21f57c0aa42618d7d Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 13:29:30 +0930 Subject: [PATCH 06/77] bwatch: add skeleton and makefile bwatch is an async block scanner that consumes blocks from bcli or any other bitcoind interface and communicates with lightningd by sending it updates. In this commit we're only introducing the plugin and some files that we will populate in future commits. --- plugins/Makefile | 21 ++++++++++++++- plugins/bwatch/bwatch.c | 44 +++++++++++++++++++++++++++++++ plugins/bwatch/bwatch.h | 21 +++++++++++++++ plugins/bwatch/bwatch_interface.c | 2 ++ plugins/bwatch/bwatch_interface.h | 12 +++++++++ plugins/bwatch/bwatch_scanner.c | 2 ++ plugins/bwatch/bwatch_scanner.h | 12 +++++++++ plugins/bwatch/bwatch_store.c | 2 ++ plugins/bwatch/bwatch_store.h | 13 +++++++++ 9 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 plugins/bwatch/bwatch.c create mode 100644 plugins/bwatch/bwatch.h create mode 100644 plugins/bwatch/bwatch_interface.c create mode 100644 plugins/bwatch/bwatch_interface.h create mode 100644 plugins/bwatch/bwatch_scanner.c create mode 100644 plugins/bwatch/bwatch_scanner.h create mode 100644 plugins/bwatch/bwatch_store.c create mode 100644 plugins/bwatch/bwatch_store.h diff --git a/plugins/Makefile b/plugins/Makefile index 6840018d98a6..7dd0f248c884 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -17,6 +17,16 @@ PLUGIN_TXPREPARE_OBJS := $(PLUGIN_TXPREPARE_SRC:.c=.o) PLUGIN_BCLI_SRC := plugins/bcli.c PLUGIN_BCLI_OBJS := $(PLUGIN_BCLI_SRC:.c=.o) +PLUGIN_BWATCH_SRC := plugins/bwatch/bwatch.c \ + plugins/bwatch/bwatch_store.c \ + plugins/bwatch/bwatch_scanner.c \ + plugins/bwatch/bwatch_interface.c +PLUGIN_BWATCH_HEADER := plugins/bwatch/bwatch.h \ + plugins/bwatch/bwatch_store.h \ + plugins/bwatch/bwatch_scanner.h \ + plugins/bwatch/bwatch_interface.h +PLUGIN_BWATCH_OBJS := $(PLUGIN_BWATCH_SRC:.c=.o) + PLUGIN_COMMANDO_SRC := plugins/commando.c PLUGIN_COMMANDO_OBJS := $(PLUGIN_COMMANDO_SRC:.c=.o) @@ -82,6 +92,7 @@ PLUGIN_ALL_SRC := \ $(PLUGIN_AUTOCLEAN_SRC) \ $(PLUGIN_chanbackup_SRC) \ $(PLUGIN_BCLI_SRC) \ + $(PLUGIN_BWATCH_SRC) \ $(PLUGIN_COMMANDO_SRC) \ $(PLUGIN_FUNDER_SRC) \ $(PLUGIN_TOPOLOGY_SRC) \ @@ -102,12 +113,14 @@ PLUGIN_ALL_HEADER := \ $(PLUGIN_FUNDER_HEADER) \ $(PLUGIN_PAY_LIB_HEADER) \ $(PLUGIN_OFFERS_HEADER) \ - $(PLUGIN_SPENDER_HEADER) + $(PLUGIN_SPENDER_HEADER) \ + $(PLUGIN_BWATCH_HEADER) C_PLUGINS := \ plugins/autoclean \ plugins/chanbackup \ plugins/bcli \ + plugins/bwatch/bwatch \ plugins/commando \ plugins/funder \ plugins/topology \ @@ -185,6 +198,12 @@ plugins/exposesecret: $(PLUGIN_EXPOSESECRET_OBJS) $(PLUGIN_LIB_OBJS) libcommon.a plugins/bcli: $(PLUGIN_BCLI_OBJS) $(PLUGIN_LIB_OBJS) libcommon.a +plugins/bwatch/bwatch.o: $(PLUGIN_BWATCH_HEADER) +plugins/bwatch/bwatch_store.o: $(PLUGIN_BWATCH_HEADER) +plugins/bwatch/bwatch_scanner.o: $(PLUGIN_BWATCH_HEADER) +plugins/bwatch/bwatch_interface.o: $(PLUGIN_BWATCH_HEADER) +plugins/bwatch/bwatch: $(PLUGIN_BWATCH_OBJS) $(PLUGIN_LIB_OBJS) libcommon.a + plugins/keysend: $(PLUGIN_KEYSEND_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_PAY_LIB_OBJS) libcommon.a $(PLUGIN_KEYSEND_OBJS): $(PLUGIN_PAY_LIB_HEADER) libcommon.a diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c new file mode 100644 index 000000000000..0c3935a8e7e3 --- /dev/null +++ b/plugins/bwatch/bwatch.c @@ -0,0 +1,44 @@ +#include "config.h" +#include +#include +#include +#include +#include + +struct bwatch *bwatch_of(struct plugin *plugin) +{ + return plugin_get_data(plugin, struct bwatch); +} + +static const char *init(struct command *cmd, + const char *buf UNUSED, + const jsmntok_t *config UNUSED) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + + bwatch->plugin = cmd->plugin; + return NULL; +} + +static const struct plugin_command commands[] = { + /* Subsequent commits register addwatch / delwatch / listwatch here. */ +}; + +int main(int argc, char *argv[]) +{ + struct bwatch *bwatch; + + setup_locale(); + bwatch = tal(NULL, struct bwatch); + bwatch->poll_interval_ms = 30000; + + plugin_main(argv, init, take(bwatch), PLUGIN_RESTARTABLE, true, NULL, + commands, ARRAY_SIZE(commands), + NULL, 0, + NULL, 0, + NULL, 0, + plugin_option("bwatch-poll-interval", "int", + "Milliseconds between chain polls (default: 30000)", + u32_option, u32_jsonfmt, &bwatch->poll_interval_ms), + NULL); +} diff --git a/plugins/bwatch/bwatch.h b/plugins/bwatch/bwatch.h new file mode 100644 index 000000000000..f307e09ed4df --- /dev/null +++ b/plugins/bwatch/bwatch.h @@ -0,0 +1,21 @@ +#ifndef LIGHTNING_PLUGINS_BWATCH_BWATCH_H +#define LIGHTNING_PLUGINS_BWATCH_BWATCH_H + +#include "config.h" +#include + +/* Main bwatch state. + * + * bwatch is an out-of-process block scanner: it polls bitcoind, parses each + * new block, and notifies lightningd (via the watchman RPCs) about chain + * activity that lightningd has registered watches for. Subsequent commits + * add the watch hash tables, block history, and polling timer fields. */ +struct bwatch { + struct plugin *plugin; + u32 poll_interval_ms; +}; + +/* Helper: retrieve the bwatch state from a plugin handle. */ +struct bwatch *bwatch_of(struct plugin *plugin); + +#endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_H */ diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c new file mode 100644 index 000000000000..aba4a22132ba --- /dev/null +++ b/plugins/bwatch/bwatch_interface.c @@ -0,0 +1,2 @@ +#include "config.h" +#include diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h new file mode 100644 index 000000000000..944a66f00f6c --- /dev/null +++ b/plugins/bwatch/bwatch_interface.h @@ -0,0 +1,12 @@ +#ifndef LIGHTNING_PLUGINS_BWATCH_BWATCH_INTERFACE_H +#define LIGHTNING_PLUGINS_BWATCH_BWATCH_INTERFACE_H + +#include "config.h" +#include + +/* Outward-facing interface from bwatch to lightningd. + * + * Subsequent commits add the watch_found / watch_revert / block_processed + * notifications and the addwatch / delwatch / listwatch RPC commands. */ + +#endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_INTERFACE_H */ diff --git a/plugins/bwatch/bwatch_scanner.c b/plugins/bwatch/bwatch_scanner.c new file mode 100644 index 000000000000..9eff596486cf --- /dev/null +++ b/plugins/bwatch/bwatch_scanner.c @@ -0,0 +1,2 @@ +#include "config.h" +#include diff --git a/plugins/bwatch/bwatch_scanner.h b/plugins/bwatch/bwatch_scanner.h new file mode 100644 index 000000000000..ac4e62d16a5d --- /dev/null +++ b/plugins/bwatch/bwatch_scanner.h @@ -0,0 +1,12 @@ +#ifndef LIGHTNING_PLUGINS_BWATCH_BWATCH_SCANNER_H +#define LIGHTNING_PLUGINS_BWATCH_BWATCH_SCANNER_H + +#include "config.h" +#include + +/* Block scanning layer for bwatch. + * + * Subsequent commits add per-watch-type matchers that walk a block's + * transactions and fire watch_found notifications back to lightningd. */ + +#endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_SCANNER_H */ diff --git a/plugins/bwatch/bwatch_store.c b/plugins/bwatch/bwatch_store.c new file mode 100644 index 000000000000..2d7742e2ffeb --- /dev/null +++ b/plugins/bwatch/bwatch_store.c @@ -0,0 +1,2 @@ +#include "config.h" +#include diff --git a/plugins/bwatch/bwatch_store.h b/plugins/bwatch/bwatch_store.h new file mode 100644 index 000000000000..566c0ff838d9 --- /dev/null +++ b/plugins/bwatch/bwatch_store.h @@ -0,0 +1,13 @@ +#ifndef LIGHTNING_PLUGINS_BWATCH_BWATCH_STORE_H +#define LIGHTNING_PLUGINS_BWATCH_BWATCH_STORE_H + +#include "config.h" +#include + +/* Block-history and watch storage layer for bwatch. + * + * Subsequent commits populate this with hash tables for each watch type + * (scriptpubkey, outpoint, scid, blockdepth) plus the lightningd datastore + * persistence helpers. */ + +#endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_STORE_H */ From 10d8ee781d85aa0d4c258d7e9ca592939806bfeb Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 13:30:29 +0930 Subject: [PATCH 07/77] bwatch: add wire format This wire file primarily contains datastructures that is used to serialize data for storing in the datastore. We have 2 types of datastores for bwatch. The block history datastore and the watch datastore. For block history we store height, the hash and the hash of the previous block. For watches we have 4 types of watches - utxo, scriptpubkey, scid and blockdepth watches, each of these have their unique info stored in the datastore. The common info for all watches includes the start block and the list of owners interested in watching. --- plugins/Makefile | 7 +++++-- plugins/bwatch/bwatch_wire.csv | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 plugins/bwatch/bwatch_wire.csv diff --git a/plugins/Makefile b/plugins/Makefile index 7dd0f248c884..813626f01a10 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -20,11 +20,13 @@ PLUGIN_BCLI_OBJS := $(PLUGIN_BCLI_SRC:.c=.o) PLUGIN_BWATCH_SRC := plugins/bwatch/bwatch.c \ plugins/bwatch/bwatch_store.c \ plugins/bwatch/bwatch_scanner.c \ - plugins/bwatch/bwatch_interface.c + plugins/bwatch/bwatch_interface.c \ + plugins/bwatch/bwatch_wiregen.c PLUGIN_BWATCH_HEADER := plugins/bwatch/bwatch.h \ plugins/bwatch/bwatch_store.h \ plugins/bwatch/bwatch_scanner.h \ - plugins/bwatch/bwatch_interface.h + plugins/bwatch/bwatch_interface.h \ + plugins/bwatch/bwatch_wiregen.h PLUGIN_BWATCH_OBJS := $(PLUGIN_BWATCH_SRC:.c=.o) PLUGIN_COMMANDO_SRC := plugins/commando.c @@ -202,6 +204,7 @@ plugins/bwatch/bwatch.o: $(PLUGIN_BWATCH_HEADER) plugins/bwatch/bwatch_store.o: $(PLUGIN_BWATCH_HEADER) plugins/bwatch/bwatch_scanner.o: $(PLUGIN_BWATCH_HEADER) plugins/bwatch/bwatch_interface.o: $(PLUGIN_BWATCH_HEADER) +plugins/bwatch/bwatch_wiregen.o: $(PLUGIN_BWATCH_HEADER) plugins/bwatch/bwatch: $(PLUGIN_BWATCH_OBJS) $(PLUGIN_LIB_OBJS) libcommon.a plugins/keysend: $(PLUGIN_KEYSEND_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_PAY_LIB_OBJS) libcommon.a diff --git a/plugins/bwatch/bwatch_wire.csv b/plugins/bwatch/bwatch_wire.csv new file mode 100644 index 000000000000..570e0b59da39 --- /dev/null +++ b/plugins/bwatch/bwatch_wire.csv @@ -0,0 +1,36 @@ +#include +#include + +# Block record: complete serializable structure +subtype,block_record_wire +subtypedata,block_record_wire,height,u32, +subtypedata,block_record_wire,hash,bitcoin_blkid, +subtypedata,block_record_wire,prev_hash,bitcoin_blkid, + +# Watch: complete serializable structure +# Type is stored to enable reconstruction of watch key from wire data +subtype,watch_wire +subtypedata,watch_wire,type,u32, +# Scriptpubkey key (for WATCH_SCRIPTPUBKEY) +subtypedata,watch_wire,scriptpubkey_len,u32, +subtypedata,watch_wire,scriptpubkey,u8,scriptpubkey_len +# Outpoint key (for WATCH_OUTPOINT) +subtypedata,watch_wire,outpoint,bitcoin_outpoint, +# SCID key (for WATCH_SCID) +subtypedata,watch_wire,scid_blockheight,u32, +subtypedata,watch_wire,scid_txindex,u32, +subtypedata,watch_wire,scid_outnum,u32, +# Blockdepth key (for WATCH_BLOCKDEPTH): block where the tx confirmed +subtypedata,watch_wire,blockdepth,u32, +# Common fields +subtypedata,watch_wire,start_block,u32, +subtypedata,watch_wire,num_owners,u16, +subtypedata,watch_wire,owners,wirestring,num_owners + +# Messages for datastore persistence - use these to serialize/deserialize +# Each message wraps a single item for storage +msgtype,bwatch_block,1 +msgdata,bwatch_block,block,block_record_wire, + +msgtype,bwatch_watch,2 +msgdata,bwatch_watch,watch,watch_wire, From 0a021ba1170aec03511293cb7e3021f5b8af876b Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 13:30:31 +0930 Subject: [PATCH 08/77] bwatch: add typed hash tables for watches We have 4 types of watches: utxo (outpoint), scriptpubkey, scid and blockdepth. Each gets its own hash table with a key shape that makes lookups direct. --- plugins/bwatch/bwatch.c | 7 ++ plugins/bwatch/bwatch.h | 49 ++++++++++- plugins/bwatch/bwatch_store.c | 148 ++++++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_store.h | 59 ++++++++++++-- 4 files changed, 254 insertions(+), 9 deletions(-) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 0c3935a8e7e3..9ab46eed6d13 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -1,5 +1,6 @@ #include "config.h" #include +#include #include #include #include @@ -17,6 +18,12 @@ static const char *init(struct command *cmd, struct bwatch *bwatch = bwatch_of(cmd->plugin); bwatch->plugin = cmd->plugin; + + bwatch->scriptpubkey_watches = new_htable(bwatch, scriptpubkey_watches); + bwatch->outpoint_watches = new_htable(bwatch, outpoint_watches); + bwatch->scid_watches = new_htable(bwatch, scid_watches); + bwatch->blockdepth_watches = new_htable(bwatch, blockdepth_watches); + return NULL; } diff --git a/plugins/bwatch/bwatch.h b/plugins/bwatch/bwatch.h index f307e09ed4df..1052ddc33053 100644 --- a/plugins/bwatch/bwatch.h +++ b/plugins/bwatch/bwatch.h @@ -2,16 +2,57 @@ #define LIGHTNING_PLUGINS_BWATCH_BWATCH_H #include "config.h" +#include +#include #include +#include + +/* Forward declare hash table types (defined in bwatch_store.h) */ +struct scriptpubkey_watches; +struct outpoint_watches; +struct scid_watches; +struct blockdepth_watches; + +/* Watch type discriminator. */ +enum watch_type { + WATCH_SCRIPTPUBKEY, + WATCH_OUTPOINT, + WATCH_SCID, + WATCH_BLOCKDEPTH, +}; + +/* Scriptpubkey wrapper: tal-allocated bytes don't carry a length, so we + * keep them in a struct with an explicit length for hashing/equality. */ +struct scriptpubkey { + const u8 *script; + size_t len; +}; + +/* A single watch: one key plus the set of owner ids that registered it. */ +struct watch { + enum watch_type type; + u32 start_block; + wirestring **owners; + union { + struct scriptpubkey scriptpubkey; + struct bitcoin_outpoint outpoint; + struct short_channel_id scid; + } key; +}; /* Main bwatch state. * - * bwatch is an out-of-process block scanner: it polls bitcoind, parses each - * new block, and notifies lightningd (via the watchman RPCs) about chain - * activity that lightningd has registered watches for. Subsequent commits - * add the watch hash tables, block history, and polling timer fields. */ + * The four watch hash tables are typed (see bwatch_store.h) so each + * lookup hits the right key shape (script bytes / outpoint / scid / + * confirm-height) without dispatching on type at every call site. */ struct bwatch { struct plugin *plugin; + + struct scriptpubkey_watches *scriptpubkey_watches; + struct outpoint_watches *outpoint_watches; + struct scid_watches *scid_watches; + struct blockdepth_watches *blockdepth_watches; + u32 poll_interval_ms; }; diff --git a/plugins/bwatch/bwatch_store.c b/plugins/bwatch/bwatch_store.c index 2d7742e2ffeb..68ba5baaefcd 100644 --- a/plugins/bwatch/bwatch_store.c +++ b/plugins/bwatch/bwatch_store.c @@ -1,2 +1,150 @@ #include "config.h" +#include +#include #include + +const struct scriptpubkey *scriptpubkey_watch_keyof(const struct watch *w) +{ + assert(w->type == WATCH_SCRIPTPUBKEY); + return &w->key.scriptpubkey; +} + +size_t scriptpubkey_hash(const struct scriptpubkey *scriptpubkey) +{ + return siphash24(siphash_seed(), scriptpubkey->script, scriptpubkey->len); +} + +bool scriptpubkey_watch_eq(const struct watch *w, const struct scriptpubkey *scriptpubkey) +{ + return w->key.scriptpubkey.len == scriptpubkey->len && + memeq(w->key.scriptpubkey.script, scriptpubkey->len, + scriptpubkey->script, scriptpubkey->len); +} + +const struct bitcoin_outpoint *outpoint_watch_keyof(const struct watch *w) +{ + assert(w->type == WATCH_OUTPOINT); + return &w->key.outpoint; +} + +size_t outpoint_hash(const struct bitcoin_outpoint *outpoint) +{ + size_t h1 = siphash24(siphash_seed(), &outpoint->txid, sizeof(outpoint->txid)); + size_t h2 = siphash24(siphash_seed(), &outpoint->n, sizeof(outpoint->n)); + return h1 ^ h2; +} + +bool outpoint_watch_eq(const struct watch *w, const struct bitcoin_outpoint *outpoint) +{ + return bitcoin_outpoint_eq(&w->key.outpoint, outpoint); +} + +const struct short_channel_id *scid_watch_keyof(const struct watch *w) +{ + assert(w->type == WATCH_SCID); + return &w->key.scid; +} + +size_t scid_hash(const struct short_channel_id *scid) +{ + return siphash24(siphash_seed(), scid, sizeof(*scid)); +} + +bool scid_watch_eq(const struct watch *w, const struct short_channel_id *scid) +{ + return short_channel_id_eq(w->key.scid, *scid); +} + +const u32 *blockdepth_watch_keyof(const struct watch *w) +{ + assert(w->type == WATCH_BLOCKDEPTH); + return &w->start_block; +} + +size_t u32_hash(const u32 *height) +{ + return siphash24(siphash_seed(), height, sizeof(*height)); +} + +bool blockdepth_watch_eq(const struct watch *w, const u32 *height) +{ + return w->start_block == *height; +} + +const char *bwatch_get_watch_type_name(enum watch_type type) +{ + switch (type) { + case WATCH_SCRIPTPUBKEY: + return "scriptpubkey"; + case WATCH_OUTPOINT: + return "outpoint"; + case WATCH_SCID: + return "scid"; + case WATCH_BLOCKDEPTH: + return "blockdepth"; + } + abort(); +} + +void bwatch_add_watch_to_hash(struct bwatch *bwatch, struct watch *w) +{ + switch (w->type) { + case WATCH_SCRIPTPUBKEY: + scriptpubkey_watches_add(bwatch->scriptpubkey_watches, w); + return; + case WATCH_OUTPOINT: + outpoint_watches_add(bwatch->outpoint_watches, w); + return; + case WATCH_SCID: + scid_watches_add(bwatch->scid_watches, w); + return; + case WATCH_BLOCKDEPTH: + blockdepth_watches_add(bwatch->blockdepth_watches, w); + return; + } + abort(); +} + +struct watch *bwatch_get_watch(struct bwatch *bwatch, + enum watch_type type, + const struct bitcoin_outpoint *outpoint, + const u8 *scriptpubkey, + const struct short_channel_id *scid, + const u32 *confirm_height) +{ + switch (type) { + case WATCH_SCRIPTPUBKEY: { + struct scriptpubkey k = { + .script = scriptpubkey, + .len = tal_bytelen(scriptpubkey), + }; + return scriptpubkey_watches_get(bwatch->scriptpubkey_watches, &k); + } + case WATCH_OUTPOINT: + return outpoint_watches_get(bwatch->outpoint_watches, outpoint); + case WATCH_SCID: + return scid_watches_get(bwatch->scid_watches, scid); + case WATCH_BLOCKDEPTH: + return blockdepth_watches_get(bwatch->blockdepth_watches, confirm_height); + } + abort(); +} + +void bwatch_remove_watch_from_hash(struct bwatch *bwatch, struct watch *w) +{ + switch (w->type) { + case WATCH_SCRIPTPUBKEY: + scriptpubkey_watches_del(bwatch->scriptpubkey_watches, w); + return; + case WATCH_OUTPOINT: + outpoint_watches_del(bwatch->outpoint_watches, w); + return; + case WATCH_SCID: + scid_watches_del(bwatch->scid_watches, w); + return; + case WATCH_BLOCKDEPTH: + blockdepth_watches_del(bwatch->blockdepth_watches, w); + return; + } + abort(); +} diff --git a/plugins/bwatch/bwatch_store.h b/plugins/bwatch/bwatch_store.h index 566c0ff838d9..4ca600496a39 100644 --- a/plugins/bwatch/bwatch_store.h +++ b/plugins/bwatch/bwatch_store.h @@ -2,12 +2,61 @@ #define LIGHTNING_PLUGINS_BWATCH_BWATCH_STORE_H #include "config.h" +#include #include -/* Block-history and watch storage layer for bwatch. - * - * Subsequent commits populate this with hash tables for each watch type - * (scriptpubkey, outpoint, scid, blockdepth) plus the lightningd datastore - * persistence helpers. */ +/* + * Per-watch-type key/hash/eq triplets so HTABLE_DEFINE_NODUPS_TYPE can + * generate a typed hash table for each watch type. Lookups then take + * the natural key (raw script bytes, bitcoin_outpoint, short_channel_id, + * or u32 confirm height) instead of dispatching on type at every call. + */ + +const struct scriptpubkey *scriptpubkey_watch_keyof(const struct watch *w); +size_t scriptpubkey_hash(const struct scriptpubkey *scriptpubkey); +bool scriptpubkey_watch_eq(const struct watch *w, const struct scriptpubkey *scriptpubkey); + +const struct bitcoin_outpoint *outpoint_watch_keyof(const struct watch *w); +size_t outpoint_hash(const struct bitcoin_outpoint *outpoint); +bool outpoint_watch_eq(const struct watch *w, const struct bitcoin_outpoint *outpoint); + +const struct short_channel_id *scid_watch_keyof(const struct watch *w); +size_t scid_hash(const struct short_channel_id *scid); +bool scid_watch_eq(const struct watch *w, const struct short_channel_id *scid); + +const u32 *blockdepth_watch_keyof(const struct watch *w); +size_t u32_hash(const u32 *height); +bool blockdepth_watch_eq(const struct watch *w, const u32 *height); + +HTABLE_DEFINE_NODUPS_TYPE(struct watch, scriptpubkey_watch_keyof, + scriptpubkey_hash, scriptpubkey_watch_eq, + scriptpubkey_watches); + +HTABLE_DEFINE_NODUPS_TYPE(struct watch, outpoint_watch_keyof, + outpoint_hash, outpoint_watch_eq, + outpoint_watches); + +HTABLE_DEFINE_NODUPS_TYPE(struct watch, scid_watch_keyof, + scid_hash, scid_watch_eq, + scid_watches); + +HTABLE_DEFINE_NODUPS_TYPE(struct watch, blockdepth_watch_keyof, + u32_hash, blockdepth_watch_eq, + blockdepth_watches); + +/* Human-readable name of a watch type, used as the second datastore key + * component (e.g. ["bwatch", "scriptpubkey", ]) once persistence + * lands in a follow-up commit. */ +const char *bwatch_get_watch_type_name(enum watch_type type); + +/* Watch hash table operations: dispatch on watch->type. */ +void bwatch_add_watch_to_hash(struct bwatch *bwatch, struct watch *w); +struct watch *bwatch_get_watch(struct bwatch *bwatch, + enum watch_type type, + const struct bitcoin_outpoint *outpoint, + const u8 *scriptpubkey, + const struct short_channel_id *scid, + const u32 *confirm_height); +void bwatch_remove_watch_from_hash(struct bwatch *bwatch, struct watch *w); #endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_STORE_H */ From 773af77d250c87a331d23305cd86e3bc3bd806d6 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 13:30:32 +0930 Subject: [PATCH 09/77] bwatch: persist block history bwatch keeps a tail of recent blocks (height, hash, prev hash) so it can detect and unwind reorgs without re-fetching from bitcoind. The datastore key for each block is zero-padded to 10 digits so listdatastore returns blocks in ascending height order. On startup we replay the stored history and resume from the most recent block. --- plugins/bwatch/bwatch.c | 9 ++ plugins/bwatch/bwatch.h | 14 ++++ plugins/bwatch/bwatch_store.c | 150 ++++++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_store.h | 13 +++ 4 files changed, 186 insertions(+) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 9ab46eed6d13..3c6ceb6c6c8b 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -5,6 +5,7 @@ #include #include #include +#include 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 1052ddc33053..6a15ff80aede 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 #include #include #include @@ -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 68ba5baaefcd..6ae177c4e5ca 100644 --- a/plugins/bwatch/bwatch_store.c +++ b/plugins/bwatch/bwatch_store.c @@ -1,7 +1,14 @@ #include "config.h" #include +#include #include +#include +#include +#include +#include +#include #include +#include 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 4ca600496a39..b3e4804f558f 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 */ From 1426629e0c1869d1c6642f46674c202b2d745fa6 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:45:31 +0930 Subject: [PATCH 10/77] bwatch: persist watches Each watch (and its set of owners) is serialized through the wire format from the earlier commit and stored in the datastore. On startup we walk each type's prefix and reload the watches into their respective hash tables, so a restart resumes watching the same things without anyone re-registering. --- plugins/bwatch/bwatch.c | 1 + plugins/bwatch/bwatch_store.c | 197 +++++++++++++++++++++++++++++++++- plugins/bwatch/bwatch_store.h | 9 +- 3 files changed, 202 insertions(+), 5 deletions(-) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 3c6ceb6c6c8b..3830a3730095 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -32,6 +32,7 @@ static const char *init(struct command *cmd, * 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); + bwatch_load_watches_from_datastore(cmd, bwatch); return NULL; } diff --git a/plugins/bwatch/bwatch_store.c b/plugins/bwatch/bwatch_store.c index 6ae177c4e5ca..f9766e0dff2a 100644 --- a/plugins/bwatch/bwatch_store.c +++ b/plugins/bwatch/bwatch_store.c @@ -156,9 +156,7 @@ 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. */ +/* List all datastore entries under a key prefix (up to 2 components). */ static const jsmntok_t *bwatch_list_datastore(const tal_t *ctx, struct command *cmd, const char *key1, const char *key2, @@ -298,3 +296,196 @@ void bwatch_load_block_history(struct command *cmd, struct bwatch *bwatch) sizeof(bwatch->current_blockhash)); } } + +static char *fmt_scriptpubkey(const tal_t *ctx, + const struct scriptpubkey *scriptpubkey) +{ + return tal_hexstr(ctx, scriptpubkey->script, scriptpubkey->len); +} + +/* Build the datastore key path for a watch. All watch types share the + * ["bwatch", , ] layout; only the key payload + * varies. */ +static const char **get_watch_datastore_key(const tal_t *ctx, const struct watch *w) +{ + const char *type_name = bwatch_get_watch_type_name(w->type); + + switch (w->type) { + case WATCH_SCRIPTPUBKEY: { + return mkdatastorekey(ctx, "bwatch", type_name, + take(fmt_scriptpubkey(NULL, &w->key.scriptpubkey))); + } + case WATCH_OUTPOINT: + return mkdatastorekey(ctx, "bwatch", type_name, + take(fmt_bitcoin_outpoint(NULL, &w->key.outpoint))); + case WATCH_SCID: + return mkdatastorekey(ctx, "bwatch", type_name, + take(fmt_short_channel_id(NULL, w->key.scid))); + case WATCH_BLOCKDEPTH: + return mkdatastorekey(ctx, "bwatch", type_name, + take(tal_fmt(NULL, "%u", w->start_block))); + } + abort(); +} + +static struct watch_wire *watch_to_wire(const tal_t *ctx, const struct watch *w) +{ + struct watch_wire *wire = tal(ctx, struct watch_wire); + size_t num_owners; + + wire->type = w->type; + wire->start_block = w->start_block; + + wire->scriptpubkey = NULL; + memset(&wire->outpoint, 0, sizeof(wire->outpoint)); + wire->scid_blockheight = wire->scid_txindex = wire->scid_outnum = 0; + wire->blockdepth = 0; + + switch (w->type) { + case WATCH_SCRIPTPUBKEY: + wire->scriptpubkey = tal_dup_arr(wire, u8, + w->key.scriptpubkey.script, + w->key.scriptpubkey.len, 0); + break; + case WATCH_OUTPOINT: + wire->outpoint = w->key.outpoint; + break; + case WATCH_SCID: + wire->scid_blockheight = short_channel_id_blocknum(w->key.scid); + wire->scid_txindex = short_channel_id_txnum(w->key.scid); + wire->scid_outnum = short_channel_id_outnum(w->key.scid); + break; + case WATCH_BLOCKDEPTH: + wire->blockdepth = w->start_block; + break; + } + + num_owners = tal_count(w->owners); + wire->owners = tal_arr(wire, wirestring *, num_owners); + for (size_t i = 0; i < num_owners; i++) + wire->owners[i] = tal_strdup(wire->owners, w->owners[i]); + + return wire; +} + +static struct watch *watch_from_wire(const tal_t *ctx, const struct watch_wire *wire) +{ + struct watch *w = tal(ctx, struct watch); + size_t num_owners; + + w->type = wire->type; + w->start_block = wire->start_block; + + switch (wire->type) { + case WATCH_SCRIPTPUBKEY: + w->key.scriptpubkey.len = tal_bytelen(wire->scriptpubkey); + w->key.scriptpubkey.script = tal_dup_arr(w, u8, wire->scriptpubkey, + w->key.scriptpubkey.len, 0); + break; + case WATCH_OUTPOINT: + w->key.outpoint = wire->outpoint; + break; + case WATCH_SCID: + if (!mk_short_channel_id(&w->key.scid, + wire->scid_blockheight, + wire->scid_txindex, + wire->scid_outnum)) + return tal_free(w); + break; + case WATCH_BLOCKDEPTH: + w->start_block = wire->blockdepth; + break; + } + + num_owners = tal_count(wire->owners); + w->owners = tal_arr(w, wirestring *, num_owners); + for (size_t i = 0; i < num_owners; i++) + w->owners[i] = tal_strdup(w->owners, wire->owners[i]); + + return w; +} + +static void load_watches_by_type(struct command *cmd, struct bwatch *bwatch, + enum watch_type type) +{ + const char *watch_type_name = bwatch_get_watch_type_name(type); + const char *buf; + const jsmntok_t *datastore, *t; + size_t i, count = 0; + + datastore = bwatch_list_datastore(tmpctx, cmd, "bwatch", watch_type_name, &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 watch_wire *wire; + struct watch *w; + + if (!data) + continue; + + if (!fromwire_bwatch_watch(tmpctx, data, &wire)) + continue; + + w = watch_from_wire(bwatch, wire); + if (!w || w->type != type) + continue; + + bwatch_add_watch_to_hash(bwatch, w); + count++; + } + + plugin_log(cmd->plugin, LOG_DBG, "Restored %zu %s from datastore", + count, watch_type_name); +} + +void bwatch_save_watch_to_datastore(struct command *cmd, const struct watch *w) +{ + const u8 *data = towire_bwatch_watch(tmpctx, watch_to_wire(tmpctx, w)); + const char **key = get_watch_datastore_key(tmpctx, w); + struct json_out *params = json_out_new(tmpctx); + const char *buf; + + json_out_start(params, NULL, '{'); + json_out_start(params, "key", '['); + for (size_t i = 0; i < tal_count(key); i++) + json_out_addstr(params, NULL, key[i]); + json_out_end(params, ']'); + json_out_addstr(params, "mode", "create-or-replace"); + json_out_addstr(params, "hex", tal_hex(tmpctx, data)); + json_out_end(params, '}'); + + jsonrpc_request_sync(tmpctx, cmd, "datastore", params, &buf); + + plugin_log(cmd->plugin, LOG_DBG, + "Saved watch to datastore (type=%d, num_owners=%zu)", + w->type, tal_count(w->owners)); +} + +void bwatch_delete_watch_from_datastore(struct command *cmd, const struct watch *w) +{ + const char **key = get_watch_datastore_key(tmpctx, w); + struct json_out *params = json_out_new(tmpctx); + const char *buf; + + json_out_start(params, NULL, '{'); + json_out_start(params, "key", '['); + for (size_t i = 0; i < tal_count(key); i++) + json_out_addstr(params, NULL, key[i]); + json_out_end(params, ']'); + json_out_end(params, '}'); + + jsonrpc_request_sync(tmpctx, cmd, "deldatastore", params, &buf); + + plugin_log(cmd->plugin, LOG_DBG, + "Deleted watch from datastore: ...%s", + key[tal_count(key) - 1]); +} + +void bwatch_load_watches_from_datastore(struct command *cmd, struct bwatch *bwatch) +{ + load_watches_by_type(cmd, bwatch, WATCH_SCRIPTPUBKEY); + load_watches_by_type(cmd, bwatch, WATCH_OUTPOINT); + load_watches_by_type(cmd, bwatch, WATCH_SCID); + load_watches_by_type(cmd, bwatch, WATCH_BLOCKDEPTH); +} diff --git a/plugins/bwatch/bwatch_store.h b/plugins/bwatch/bwatch_store.h index b3e4804f558f..90131e36961a 100644 --- a/plugins/bwatch/bwatch_store.h +++ b/plugins/bwatch/bwatch_store.h @@ -45,8 +45,7 @@ HTABLE_DEFINE_NODUPS_TYPE(struct watch, blockdepth_watch_keyof, blockdepth_watches); /* Human-readable name of a watch type, used as the second datastore key - * component (e.g. ["bwatch", "scriptpubkey", ]) once persistence - * lands in a follow-up commit. */ + * component (e.g. ["bwatch", "scriptpubkey", ]). */ const char *bwatch_get_watch_type_name(enum watch_type type); /* Watch hash table operations: dispatch on watch->type. */ @@ -72,4 +71,10 @@ void bwatch_add_block_to_history(struct bwatch *bwatch, u32 height, void bwatch_delete_block_from_datastore(struct command *cmd, u32 height); void bwatch_load_block_history(struct command *cmd, struct bwatch *bwatch); +/* Watch persistence: round-trip via bwatch_wiregen serialisation, + * stored under ["bwatch", , ]. */ +void bwatch_save_watch_to_datastore(struct command *cmd, const struct watch *w); +void bwatch_delete_watch_from_datastore(struct command *cmd, const struct watch *w); +void bwatch_load_watches_from_datastore(struct command *cmd, struct bwatch *bwatch); + #endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_STORE_H */ From 30350a09cb767c24f7712e091ec1083d18e35bf0 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:45:47 +0930 Subject: [PATCH 11/77] bwatch: add addwatch/delwatch helpers bwatch_add_watch and bwatch_del_watch are the high-level entry points the RPCs (added in a later commit) use. Adding a watch that already exists merges the owner list and lowers start_block if the new request needs to scan further back, so a re-registering daemon (e.g. onchaind on restart) doesn't lose missed events. Removing a watch drops only the requesting owner; the watch itself is removed once the owner list is empty. --- plugins/bwatch/bwatch_store.c | 111 ++++++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_store.h | 22 +++++++ 2 files changed, 133 insertions(+) diff --git a/plugins/bwatch/bwatch_store.c b/plugins/bwatch/bwatch_store.c index f9766e0dff2a..27d7687f6f30 100644 --- a/plugins/bwatch/bwatch_store.c +++ b/plugins/bwatch/bwatch_store.c @@ -489,3 +489,114 @@ void bwatch_load_watches_from_datastore(struct command *cmd, struct bwatch *bwat load_watches_by_type(cmd, bwatch, WATCH_SCID); load_watches_by_type(cmd, bwatch, WATCH_BLOCKDEPTH); } + +/* -1 means "not found" */ +static int find_owner(wirestring **owners, const char *owner_id) +{ + for (size_t i = 0; i < tal_count(owners); i++) { + if (streq(owners[i], owner_id)) + return i; + } + return -1; +} + +struct watch *bwatch_add_watch(struct command *cmd, + struct bwatch *bwatch, + enum watch_type type, + const struct bitcoin_outpoint *outpoint, + const u8 *scriptpubkey, + const struct short_channel_id *scid, + const u32 *confirm_height, + u32 start_block, + const char *owner_id TAKES) +{ + struct watch *w = bwatch_get_watch(bwatch, type, outpoint, scriptpubkey, + scid, confirm_height); + + if (w) { + bool lowered = start_block < w->start_block; + bool found_owner = (find_owner(w->owners, owner_id) != -1); + if (lowered) + w->start_block = start_block; + if (!found_owner) + tal_arr_expand(&w->owners, + tal_strdup(w->owners, owner_id)); + bwatch_save_watch_to_datastore(cmd, w); + /* Always rescan even if owner is already registered: stateless + * restarters (e.g. onchaind) re-register on startup and need + * missed spend events replayed. */ + plugin_log(cmd->plugin, LOG_DBG, + found_owner + ? (lowered + ? "Owner %s already watching, lowering start_block to %u" + : "Owner %s already watching, rescanning for missed events at %u") + : "Owner %s added to existing watch, start_block %u", + owner_id, w->start_block); + return w; + } + + w = tal(bwatch, struct watch); + w->type = type; + w->start_block = start_block; + switch (w->type) { + case WATCH_SCRIPTPUBKEY: + w->key.scriptpubkey.len = tal_bytelen(scriptpubkey); + w->key.scriptpubkey.script = tal_dup_talarr(w, u8, scriptpubkey); + break; + case WATCH_OUTPOINT: + w->key.outpoint = *outpoint; + break; + case WATCH_SCID: + w->key.scid = *scid; + break; + case WATCH_BLOCKDEPTH: + /* confirm_height == start_block for blockdepth watches; + * already set from start_block above. */ + break; + } + w->owners = tal_arr(w, wirestring *, 1); + w->owners[0] = tal_strdup(w->owners, owner_id); + bwatch_save_watch_to_datastore(cmd, w); + bwatch_add_watch_to_hash(bwatch, w); + return w; +} + +void bwatch_del_watch(struct command *cmd, + struct bwatch *bwatch, + enum watch_type type, + const struct bitcoin_outpoint *outpoint, + const u8 *scriptpubkey, + const struct short_channel_id *scid, + const u32 *confirm_height, + const char *owner_id) +{ + struct watch *w = bwatch_get_watch(bwatch, type, outpoint, scriptpubkey, + scid, confirm_height); + int owner_off; + + if (!w) { + plugin_log(cmd->plugin, LOG_DBG, + "Attempted to remove non-existent %s watch (already gone)", + bwatch_get_watch_type_name(type)); + return; + } + + owner_off = find_owner(w->owners, owner_id); + if (owner_off < 0) { + plugin_log(cmd->plugin, LOG_BROKEN, + "Attempted to remove watch for owner %s but it wasn't watching", + owner_id); + return; + } + + tal_free(w->owners[owner_off]); + tal_arr_remove(&w->owners, owner_off); + + if (tal_count(w->owners) == 0) { + bwatch_delete_watch_from_datastore(cmd, w); + bwatch_remove_watch_from_hash(bwatch, w); + tal_free(w); + } else { + bwatch_save_watch_to_datastore(cmd, w); + } +} diff --git a/plugins/bwatch/bwatch_store.h b/plugins/bwatch/bwatch_store.h index 90131e36961a..194d2f03ff36 100644 --- a/plugins/bwatch/bwatch_store.h +++ b/plugins/bwatch/bwatch_store.h @@ -77,4 +77,26 @@ void bwatch_save_watch_to_datastore(struct command *cmd, const struct watch *w); void bwatch_delete_watch_from_datastore(struct command *cmd, const struct watch *w); void bwatch_load_watches_from_datastore(struct command *cmd, struct bwatch *bwatch); +/* High-level add/del that combine hash-table updates and datastore writes, + * and merge owner sets / lower start_block when the same key is registered + * multiple times. */ +struct watch *bwatch_add_watch(struct command *cmd, + struct bwatch *bwatch, + enum watch_type type, + const struct bitcoin_outpoint *outpoint, + const u8 *scriptpubkey, + const struct short_channel_id *scid, + const u32 *confirm_height, + u32 start_block, + const char *owner_id TAKES); + +void bwatch_del_watch(struct command *cmd, + struct bwatch *bwatch, + enum watch_type type, + const struct bitcoin_outpoint *outpoint, + const u8 *scriptpubkey, + const struct short_channel_id *scid, + const u32 *confirm_height, + const char *owner_id); + #endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_STORE_H */ From 86bf85d5a9cbce93fff0293fbcc7c1c6864a721f Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:46:03 +0930 Subject: [PATCH 12/77] bwatch: poll chain and append blocks Add the chain-polling loop. A timer fires bwatch_poll_chain, which calls getchaininfo to learn bitcoind's tip; if we're behind, we fetch the next block via getrawblockbyheight, append it to the in-memory history and persist it to the datastore. After each successful persist we reschedule the timer at zero delay so we keep fetching back-to-back until we catch up to the chain tip. Once getchaininfo reports no new block, we settle into the steady-state cadence (30s by default, tunable via the --bwatch-poll-interval option). This commit only handles the happy path. Reorg detection, watchman notifications and watch matching land in subsequent commits. --- plugins/bwatch/bwatch.c | 187 ++++++++++++++++++++++++++++++++++++++++ plugins/bwatch/bwatch.h | 10 +++ 2 files changed, 197 insertions(+) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 3830a3730095..097375ed74f4 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -1,5 +1,9 @@ #include "config.h" #include +#include +#include +#include +#include #include #include #include @@ -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 6a15ff80aede..7a128ef50d6f 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 */ From cf1999b9f55d1f901bef77706a65cf093729555a Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:46:08 +0930 Subject: [PATCH 13/77] bwatch: notify watchman on block_processed After bwatch persists a new tip, send a block_processed RPC to watchman (lightningd) with the height and hash. bwatch only continues polling for the next block once watchman has acknowledged that it has also processed the new block height on its end. This matters for crash safety: on restart we treat watchman's height as the floor and re-fetch anything above it, so any block we acted on must be visible to watchman before we move on. If watchman isn't ready yet (e.g. lightningd still booting) the RPC errors out non-fatally; we just reschedule and retry. --- plugins/bwatch/bwatch.c | 18 ++------ plugins/bwatch/bwatch_interface.c | 75 +++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_interface.h | 10 ++++- 3 files changed, 87 insertions(+), 16 deletions(-) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 097375ed74f4..d94328a71048 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -79,20 +79,9 @@ static struct command_result *poll_finished(struct command *cmd) 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. */ + * then persist; once persisted we notify watchman, and the next poll is + * scheduled from the block_processed ack so we don't race ahead of it. */ static struct command_result *handle_block(struct command *cmd, const char *method UNUSED, const char *buf, @@ -123,7 +112,8 @@ static struct command_result *handle_block(struct command *cmd, bwatch->current_blockhash, block->hdr.prev_hash, }; - return bwatch_add_block_to_datastore(cmd, &br, fetch_more); + return bwatch_add_block_to_datastore(cmd, &br, + bwatch_send_block_processed); } /* getchaininfo response: pick the next block to fetch (or just reschedule). */ diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index aba4a22132ba..6b2893ba604b 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -1,2 +1,77 @@ #include "config.h" +#include +#include +#include #include + +/* + * ============================================================================ + * SENDING BLOCK_PROCESSED NOTIFICATION + * + * After bwatch has persisted a new tip, it tells watchman by sending the + * block_processed RPC. The next poll is scheduled from the ack callback, + * which guarantees watchman's persisted height is updated before bwatch + * looks for another block — important for crash safety: on restart we + * trust watchman's height as the floor and re-fetch anything above it. + * ============================================================================ + */ + +/* Watchman acked block_processed: safe to poll for the next block. */ +static struct command_result *block_processed_ack(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 acked_height; + const char *err; + + err = json_scan(tmpctx, buf, result, + "{blockheight:%}", + JSON_SCAN(json_to_number, &acked_height)); + if (err) + plugin_err(cmd->plugin, "block_processed ack '%.*s': %s", + json_tok_full_len(result), + json_tok_full(buf, result), err); + + plugin_log(cmd->plugin, LOG_DBG, + "Received block_processed ack for height %u", acked_height); + + bwatch->poll_timer = global_timer(cmd->plugin, time_from_sec(0), + bwatch_poll_chain, NULL); + return timer_complete(cmd); +} + +/* Non-fatal: watchman may not be ready yet (e.g. lightningd still booting). + * Reschedule the poll anyway so we keep retrying without busy-looping. */ +static struct command_result *block_processed_err(struct command *cmd, + const char *method UNUSED, + const char *buf, + const jsmntok_t *result, + void *unused UNUSED) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + + plugin_log(cmd->plugin, LOG_BROKEN, + "block_processed RPC failed (watchman not ready?): %.*s", + json_tok_full_len(result), json_tok_full(buf, result)); + + bwatch->poll_timer = global_timer(cmd->plugin, time_from_sec(0), + bwatch_poll_chain, NULL); + return timer_complete(cmd); +} + +struct command_result *bwatch_send_block_processed(struct command *cmd) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + struct out_req *req; + + req = jsonrpc_request_start(cmd, "block_processed", + block_processed_ack, block_processed_err, + NULL); + json_add_u32(req->js, "blockheight", bwatch->current_height); + json_add_string(req->js, "blockhash", + fmt_bitcoin_blkid(tmpctx, &bwatch->current_blockhash)); + return send_outreq(req); +} diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h index 944a66f00f6c..e3424885037a 100644 --- a/plugins/bwatch/bwatch_interface.h +++ b/plugins/bwatch/bwatch_interface.h @@ -6,7 +6,13 @@ /* Outward-facing interface from bwatch to lightningd. * - * Subsequent commits add the watch_found / watch_revert / block_processed - * notifications and the addwatch / delwatch / listwatch RPC commands. */ + * Subsequent commits add the watch_found / watch_revert notifications + * and the addwatch / delwatch / listwatch RPC commands. */ + +/* Send a block_processed RPC to watchman after a new block has been + * persisted. The next poll is started from the ack callback so we don't + * race ahead of watchman's view of the chain. Chains on the same poll + * command so timer_complete fires once watchman has acknowledged. */ +struct command_result *bwatch_send_block_processed(struct command *cmd); #endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_INTERFACE_H */ From fd6371b09c32a8087e01bff641a0854f4eea167d Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:46:12 +0930 Subject: [PATCH 14/77] bwatch: detect reorgs and roll back tip When handle_block fetches the next block, validate its parent hash against our current tip. If they disagree we're seeing a reorg: pop our in-memory + persisted tip via bwatch_remove_tip, walk the history one back, and re-fetch from the new height. Each fetch may itself reorg further, so the loop naturally peels off as many stale tips as needed until the chain rejoins. After every rollback, tell watchman the new tip via revert_block_processed so its persisted height tracks bwatch's. If we crash before the ack lands, watchman's stale height will be higher than ours on restart, which retriggers the rollback. If the rollback exhausts our history (we rolled back past the oldest record we still hold) we zero current_height/current_blockhash and let the next poll re-init from bitcoind's tip. Notifying owners that their watches were reverted lands in a subsequent commit. --- plugins/bwatch/bwatch.c | 77 ++++++++++++++++++++++++++++--- plugins/bwatch/bwatch_interface.c | 35 ++++++++++++++ plugins/bwatch/bwatch_interface.h | 8 ++++ 3 files changed, 114 insertions(+), 6 deletions(-) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index d94328a71048..72366ae410ea 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -79,31 +79,96 @@ static struct command_result *poll_finished(struct command *cmd) return timer_complete(cmd); } -/* Process one block fetched from bitcoind: update tip, append to history, - * then persist; once persisted we notify watchman, and the next poll is - * scheduled from the block_processed ack so we don't race ahead of it. */ +/* Remove tip block on reorg. */ +static void bwatch_remove_tip(struct command *cmd, struct bwatch *bwatch) +{ + const struct block_record_wire *newtip; + size_t count = tal_count(bwatch->block_history); + + if (count == 0) { + plugin_log(bwatch->plugin, LOG_BROKEN, + "remove_tip called with no block history!"); + return; + } + + plugin_log(bwatch->plugin, LOG_DBG, "Removing stale block %u: %s", + bwatch->current_height, + fmt_bitcoin_blkid(tmpctx, &bwatch->current_blockhash)); + + /* Delete block from datastore */ + bwatch_delete_block_from_datastore(cmd, bwatch->current_height); + + /* Remove last block from history */ + tal_resize(&bwatch->block_history, count - 1); + + /* Move tip back one */ + newtip = bwatch_last_block(bwatch); + if (newtip) { + assert(newtip->height == bwatch->current_height - 1); + bwatch->current_height = newtip->height; + bwatch->current_blockhash = newtip->hash; + + /* Tell watchman the tip rolled back so it persists the new height+hash. + * If we crash before the ack, watchman's stale height > bwatch's height + * on restart, which naturally retriggers the rollback via getwatchmanheight. */ + bwatch_send_revert_block_processed(cmd, bwatch->current_height, + &bwatch->current_blockhash); + } else { + /* History exhausted: we've rolled back past everything we stored. + * Set current_height to 0 so getwatchmanheight_done can reset it to + * watchman_height. Don't notify watchman — it already knows its own + * height and we're about to resume from there via sequential polling. */ + bwatch->current_height = 0; + memset(&bwatch->current_blockhash, 0, sizeof(bwatch->current_blockhash)); + } +} + +/* Process or initialize from a block. */ static struct command_result *handle_block(struct command *cmd, const char *method UNUSED, const char *buf, const jsmntok_t *result, - ptrint_t *block_height) + ptrint_t *block_heightptr) { struct bwatch *bwatch = bwatch_of(cmd->plugin); struct bitcoin_blkid blockhash; struct bitcoin_block *block; + bool is_init = (bwatch->current_height == 0); + u32 block_height = ptr2int(block_heightptr); 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), + block_height, json_tok_full_len(result), json_tok_full(buf, result)); return poll_finished(cmd); } - bwatch->current_height = ptr2int(block_height); + if (!is_init) { + /* Verify the parent of the new block is our current tip; if + * not, we have a reorg. Pop the tip and refetch the block + * until we find a common ancestor, then roll forward from + * there. Skip when history is empty (rollback exhausted it). */ + if (tal_count(bwatch->block_history) > 0 && + !bitcoin_blkid_eq(&block->hdr.prev_hash, &bwatch->current_blockhash)) { + plugin_log(cmd->plugin, LOG_INFORM, + "Reorg detected at block %u: expected parent %s, got %s (fetched block hash: %s)", + block_height, + fmt_bitcoin_blkid(tmpctx, &bwatch->current_blockhash), + fmt_bitcoin_blkid(tmpctx, &block->hdr.prev_hash), + fmt_bitcoin_blkid(tmpctx, &blockhash)); + bwatch_remove_tip(cmd, bwatch); + return fetch_block_handle(cmd, bwatch->current_height + 1); + } + } + + /* Update state */ + bwatch->current_height = block_height; bwatch->current_blockhash = blockhash; + + /* Update in-memory history immediately */ bwatch_add_block_to_history(bwatch, bwatch->current_height, &blockhash, &block->hdr.prev_hash); diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index 6b2893ba604b..623046a5e269 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -75,3 +75,38 @@ struct command_result *bwatch_send_block_processed(struct command *cmd) fmt_bitcoin_blkid(tmpctx, &bwatch->current_blockhash)); return send_outreq(req); } + +/* + * ============================================================================ + * REVERT BLOCK NOTIFICATION + * ============================================================================ + */ + +/* Generic fire-and-forget ack: aux notifications don't gate the poll, so + * we just close the aux command on either success or error. */ +static struct command_result *notify_ack(struct command *cmd, + const char *method UNUSED, + const char *buf UNUSED, + const jsmntok_t *result UNUSED, + void *arg UNUSED) +{ + return aux_command_done(cmd); +} + +/* Notify watchman that a block was rolled back so it can update and persist + * its tip. Fire-and-forget via aux_command — the poll timer doesn't depend + * on the ack. Crash safety: if we crash before the ack, watchman's stale + * height will be higher than bwatch's on restart, retriggering rollback. */ +void bwatch_send_revert_block_processed(struct command *cmd, u32 new_height, + const struct bitcoin_blkid *new_hash) +{ + struct command *aux = aux_command(cmd); + struct out_req *req; + + req = jsonrpc_request_start(aux, "revert_block_processed", + notify_ack, notify_ack, NULL); + json_add_u32(req->js, "blockheight", new_height); + json_add_string(req->js, "blockhash", + fmt_bitcoin_blkid(tmpctx, new_hash)); + send_outreq(req); +} diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h index e3424885037a..30bd1252b08d 100644 --- a/plugins/bwatch/bwatch_interface.h +++ b/plugins/bwatch/bwatch_interface.h @@ -15,4 +15,12 @@ * command so timer_complete fires once watchman has acknowledged. */ struct command_result *bwatch_send_block_processed(struct command *cmd); +/* Notify watchman that the tip has been rolled back during a reorg, so + * watchman can update and persist its own height. Fire-and-forget via + * an aux_command — the poll timer doesn't depend on this ack. Crash + * safety: if we crash before the ack lands, watchman's stale height will + * be higher than bwatch's on restart, which retriggers the rollback. */ +void bwatch_send_revert_block_processed(struct command *cmd, u32 new_height, + const struct bitcoin_blkid *new_hash); + #endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_INTERFACE_H */ From d20de1152b7ed9dceb5fb88bb6e6150a00980c94 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:46:15 +0930 Subject: [PATCH 15/77] bwatch: add watch_found and watch_revert notifications Add two RPCs for surfacing watches to lightningd on a new block or reorg. bwatch_send_watch_found informs lightningd of any watches that were found in the current processed block. The owner is used to disambiguate watches that may pertain to multiple subdaemons. bwatch_send_watch_revert is sent in case of a revert; it informs the owner that a previously reported watch has been rolled back. These functions get wired up in subsequent commits. --- plugins/bwatch/bwatch_interface.c | 76 ++++++++++++++++++++++++++----- plugins/bwatch/bwatch_interface.h | 17 +++++-- 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index 623046a5e269..e3d0f9b93bbb 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -4,6 +4,71 @@ #include #include +/* + * ============================================================================ + * SENDING WATCH_FOUND NOTIFICATIONS + * ============================================================================ + */ + +/* Callback for watch_found RPC. + * watch_found notifications are sent on an aux command so they cannot + * interfere with the poll command lifetime. */ +static struct command_result *notify_ack(struct command *cmd, + const char *method UNUSED, + const char *buf UNUSED, + const jsmntok_t *result UNUSED, + void *arg UNUSED) +{ + return aux_command_done(cmd); +} + +/* Send watch_found notification to lightningd. */ +void bwatch_send_watch_found(struct command *cmd, + const struct bitcoin_tx *tx, + u32 blockheight, + const struct watch *w, + u32 txindex, + u32 index) +{ + struct command *aux = aux_command(cmd); + struct out_req *req; + + req = jsonrpc_request_start(aux, "watch_found", + notify_ack, notify_ack, NULL); + /* tx==NULL signals "not found" for WATCH_SCID; omit tx+txindex so + * json_watch_found passes tx=NULL down to the handler. */ + if (tx) { + json_add_tx(req->js, "tx", tx); + json_add_u32(req->js, "txindex", txindex); + if (index != UINT32_MAX) + json_add_u32(req->js, "index", index); + } + json_add_u32(req->js, "blockheight", blockheight); + + /* Add owners array */ + json_array_start(req->js, "owners"); + for (size_t i = 0; i < tal_count(w->owners); i++) + json_add_string(req->js, NULL, w->owners[i]); + json_array_end(req->js); + + send_outreq(req); +} + +/* Tell one owner that a previously-reported watch_found was rolled back. */ +void bwatch_send_watch_revert(struct command *cmd, + const char *owner, + u32 blockheight) +{ + struct command *aux = aux_command(cmd); + struct out_req *req; + + req = jsonrpc_request_start(aux, "watch_revert", + notify_ack, notify_ack, NULL); + json_add_string(req->js, "owner", owner); + json_add_u32(req->js, "blockheight", blockheight); + send_outreq(req); +} + /* * ============================================================================ * SENDING BLOCK_PROCESSED NOTIFICATION @@ -82,17 +147,6 @@ struct command_result *bwatch_send_block_processed(struct command *cmd) * ============================================================================ */ -/* Generic fire-and-forget ack: aux notifications don't gate the poll, so - * we just close the aux command on either success or error. */ -static struct command_result *notify_ack(struct command *cmd, - const char *method UNUSED, - const char *buf UNUSED, - const jsmntok_t *result UNUSED, - void *arg UNUSED) -{ - return aux_command_done(cmd); -} - /* Notify watchman that a block was rolled back so it can update and persist * its tip. Fire-and-forget via aux_command — the poll timer doesn't depend * on the ack. Crash safety: if we crash before the ack, watchman's stale diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h index 30bd1252b08d..092d3f211ba8 100644 --- a/plugins/bwatch/bwatch_interface.h +++ b/plugins/bwatch/bwatch_interface.h @@ -4,10 +4,19 @@ #include "config.h" #include -/* Outward-facing interface from bwatch to lightningd. - * - * Subsequent commits add the watch_found / watch_revert notifications - * and the addwatch / delwatch / listwatch RPC commands. */ +/* Outward-facing interface from bwatch to lightningd. */ + +/* Send watch_found notification to lightningd */ +void bwatch_send_watch_found(struct command *cmd, + const struct bitcoin_tx *tx, + u32 blockheight, + const struct watch *w, + u32 txindex, + u32 index); + +void bwatch_send_watch_revert(struct command *cmd, + const char *owner, + u32 blockheight); /* Send a block_processed RPC to watchman after a new block has been * persisted. The next poll is started from the ack callback so we don't From 437dccd433cbc3c5933042a8ba663a98586157ea Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:46:32 +0930 Subject: [PATCH 16/77] bwatch: scan blocks for scriptpubkey and outpoint matches After every fetched block, walk each transaction and fire watch_found for matching scriptpubkey outputs and spent outpoints. Outputs are matched by hash lookup against scriptpubkey_watches; inputs by reconstructing the spent outpoint and looking it up in outpoint_watches. --- plugins/bwatch/bwatch.c | 3 + plugins/bwatch/bwatch_scanner.c | 100 ++++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_scanner.h | 11 ++-- 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 72366ae410ea..3a6b05c08c59 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -162,6 +162,9 @@ static struct command_result *handle_block(struct command *cmd, bwatch_remove_tip(cmd, bwatch); return fetch_block_handle(cmd, bwatch->current_height + 1); } + + bwatch_process_block_txs(cmd, bwatch, block, block_height, + &blockhash); } /* Update state */ diff --git a/plugins/bwatch/bwatch_scanner.c b/plugins/bwatch/bwatch_scanner.c index 9eff596486cf..d64c71386779 100644 --- a/plugins/bwatch/bwatch_scanner.c +++ b/plugins/bwatch/bwatch_scanner.c @@ -1,2 +1,102 @@ #include "config.h" +#include +#include +#include #include +#include +#include + +/* + * ============================================================================ + * TRANSACTION WATCH CHECKING + * ============================================================================ + */ + +/* Check all scriptpubkey watches via hash lookup */ +static void check_scriptpubkey_watches(struct command *cmd, + struct bwatch *bwatch, + const struct bitcoin_tx *tx, + u32 blockheight, + const struct bitcoin_blkid *blockhash, + u32 txindex) +{ + struct bitcoin_txid txid; + + bitcoin_txid(tx, &txid); + + for (size_t i = 0; i < tx->wtx->num_outputs; i++) { + struct watch *w; + struct scriptpubkey k = { + .script = tx->wtx->outputs[i].script, + .len = tx->wtx->outputs[i].script_len + }; + + w = scriptpubkey_watches_get(bwatch->scriptpubkey_watches, &k); + if (!w) + continue; + if (w->start_block != UINT32_MAX + && blockheight < w->start_block) { + plugin_log(cmd->plugin, LOG_BROKEN, + "Watch for script %s on height >= %u found on block %u???", + tal_hexstr(tmpctx, k.script, k.len), + w->start_block, blockheight); + continue; + } + bwatch_send_watch_found(cmd, tx, blockheight, w, txindex, i); + } +} + +/* Check all outpoint watches via hash lookup */ +static void check_outpoint_watches(struct command *cmd, + struct bwatch *bwatch, + const struct bitcoin_tx *tx, + u32 blockheight, + const struct bitcoin_blkid *blockhash, + u32 txindex) +{ + for (size_t i = 0; i < tx->wtx->num_inputs; i++) { + struct watch *w; + struct bitcoin_outpoint outpoint; + + bitcoin_tx_input_get_txid(tx, i, &outpoint.txid); + outpoint.n = tx->wtx->inputs[i].index; + + w = outpoint_watches_get(bwatch->outpoint_watches, &outpoint); + if (!w) + continue; + if (w->start_block != UINT32_MAX + && blockheight < w->start_block) { + plugin_log(cmd->plugin, LOG_BROKEN, + "Watch for outpoint %s on height >= %u found on block %u???", + fmt_bitcoin_outpoint(tmpctx, &outpoint), + w->start_block, blockheight); + continue; + } + bwatch_send_watch_found(cmd, tx, blockheight, w, txindex, i); + } +} + +/* Check a tx against all watches (during normal block processing). + * UTXO spend tracking is handled by lightningd via outpoint watches + * (wallet/utxo/ fires wallet_utxo_spent_watch_found). */ +static void check_tx_against_all_watches(struct command *cmd, + struct bwatch *bwatch, + const struct bitcoin_tx *tx, + u32 blockheight, + const struct bitcoin_blkid *blockhash, + u32 txindex) +{ + check_scriptpubkey_watches(cmd, bwatch, tx, blockheight, blockhash, txindex); + check_outpoint_watches(cmd, bwatch, tx, blockheight, blockhash, txindex); +} + +void bwatch_process_block_txs(struct command *cmd, + struct bwatch *bwatch, + const struct bitcoin_block *block, + u32 blockheight, + const struct bitcoin_blkid *blockhash) +{ + for (size_t i = 0; i < tal_count(block->tx); i++) + check_tx_against_all_watches(cmd, bwatch, block->tx[i], + blockheight, blockhash, i); +} diff --git a/plugins/bwatch/bwatch_scanner.h b/plugins/bwatch/bwatch_scanner.h index ac4e62d16a5d..3d52c0639abb 100644 --- a/plugins/bwatch/bwatch_scanner.h +++ b/plugins/bwatch/bwatch_scanner.h @@ -4,9 +4,12 @@ #include "config.h" #include -/* Block scanning layer for bwatch. - * - * Subsequent commits add per-watch-type matchers that walk a block's - * transactions and fire watch_found notifications back to lightningd. */ +/* Scan every transaction in a block against the active scriptpubkey + * and outpoint watches, firing watch_found for each match. */ +void bwatch_process_block_txs(struct command *cmd, + struct bwatch *bwatch, + const struct bitcoin_block *block, + u32 blockheight, + const struct bitcoin_blkid *blockhash); #endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_SCANNER_H */ From c7b5571dfe450f3f2d3bcc5cac8aed58fd1609c9 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:46:36 +0930 Subject: [PATCH 17/77] bwatch: scan blocks for scid matches After the per-tx scriptpubkey/outpoint pass, walk every scid watch and fire watch_found for any whose encoded blockheight matches the block just processed. The watch's scid encodes the expected (txindex, outnum), so we jump straight there without scanning. If the position is out of range (txindex past the block, or outnum past the tx) we send watch_found with tx=NULL, which lightningd treats as the "not found" case. --- plugins/bwatch/bwatch_scanner.c | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/plugins/bwatch/bwatch_scanner.c b/plugins/bwatch/bwatch_scanner.c index d64c71386779..3dc13d9691b9 100644 --- a/plugins/bwatch/bwatch_scanner.c +++ b/plugins/bwatch/bwatch_scanner.c @@ -90,6 +90,65 @@ static void check_tx_against_all_watches(struct command *cmd, check_outpoint_watches(cmd, bwatch, tx, blockheight, blockhash, txindex); } +/* Fire watch_found for a scid watch anchored to this block. */ +static void maybe_fire_scid_watch(struct command *cmd, + const struct bitcoin_block *block, + u32 blockheight, + const struct watch *w) +{ + struct bitcoin_tx *tx; + u32 scid_blockheight, txindex, outnum; + + assert(w->type == WATCH_SCID); + + /* The scid pins the watch to one specific block. */ + scid_blockheight = short_channel_id_blocknum(w->key.scid); + if (scid_blockheight != blockheight) + return; + + txindex = short_channel_id_txnum(w->key.scid); + outnum = short_channel_id_outnum(w->key.scid); + + /* Out-of-range (txindex or outnum) means the scid doesn't match + * anything on this chain; fire watch_found with tx=NULL so + * lightningd cleans the watch up. */ + if (txindex >= tal_count(block->tx)) { + plugin_log(cmd->plugin, LOG_BROKEN, + "scid watch blockheight=%u txindex=%u outnum=%u: txindex out of range (block has %zu txs)", + blockheight, txindex, outnum, tal_count(block->tx)); + bwatch_send_watch_found(cmd, NULL, blockheight, w, txindex, outnum); + return; + } + tx = block->tx[txindex]; + if (outnum >= tx->wtx->num_outputs) { + plugin_log(cmd->plugin, LOG_BROKEN, + "scid watch blockheight=%u txindex=%u outnum=%u: outnum out of range (tx has %zu outputs)", + blockheight, txindex, outnum, tx->wtx->num_outputs); + bwatch_send_watch_found(cmd, NULL, blockheight, w, txindex, outnum); + return; + } + + /* Found it: tell lightningd the scid output is confirmed. */ + bwatch_send_watch_found(cmd, tx, blockheight, w, txindex, outnum); +} + +/* Walk every scid watch and fire watch_found for any whose encoded + * blockheight matches this block. */ +static void check_scid_watches(struct command *cmd, + struct bwatch *bwatch, + const struct bitcoin_block *block, + u32 blockheight) +{ + struct scid_watches_iter it; + struct watch *scid_w; + + for (scid_w = scid_watches_first(bwatch->scid_watches, &it); + scid_w; + scid_w = scid_watches_next(bwatch->scid_watches, &it)) { + maybe_fire_scid_watch(cmd, block, blockheight, scid_w); + } +} + void bwatch_process_block_txs(struct command *cmd, struct bwatch *bwatch, const struct bitcoin_block *block, @@ -99,4 +158,6 @@ void bwatch_process_block_txs(struct command *cmd, for (size_t i = 0; i < tal_count(block->tx); i++) check_tx_against_all_watches(cmd, bwatch, block->tx[i], blockheight, blockhash, i); + + check_scid_watches(cmd, bwatch, block, blockheight); } From 8693634fca777d7a2a961de7c4243509af6e4932 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:47:43 +0930 Subject: [PATCH 18/77] bwatch: fire blockdepth notifications per block Subdaemons like channel_open and onchaind care about confirmation depth, not the underlying tx. Walk blockdepth_watches on every new block and send watch_found with the current depth to each owner. This is what keeps bwatch awake in environments like Greenlight, where we'd otherwise prefer to hibernate: as long as something is waiting on a confirmation milestone, the blockdepth watch holds the poll open; once it's deleted, we're free to sleep again. Depth fires before the per-tx scan so restart-marker watches get a chance to spin up subdaemons before any outpoint hits land for the same block. Watches whose start_block is ahead of the tip are stale (reorged-away, awaiting delete) and skipped. --- plugins/bwatch/bwatch.c | 4 ++++ plugins/bwatch/bwatch_interface.c | 23 +++++++++++++++++++++++ plugins/bwatch/bwatch_interface.h | 6 ++++++ plugins/bwatch/bwatch_scanner.c | 23 +++++++++++++++++++++++ plugins/bwatch/bwatch_scanner.h | 6 ++++++ 5 files changed, 62 insertions(+) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 3a6b05c08c59..1b31f98b98ec 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -163,6 +163,10 @@ static struct command_result *handle_block(struct command *cmd, return fetch_block_handle(cmd, bwatch->current_height + 1); } + /* Depth first: restart-marker watches (e.g. onchaind/ + * channel_close) start subdaemons before outpoint watches + * fire for the same block. */ + bwatch_check_blockdepth_watches(cmd, bwatch, block_height); bwatch_process_block_txs(cmd, bwatch, block, block_height, &blockhash); } diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index e3d0f9b93bbb..a1be223a3bfe 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -54,6 +54,29 @@ void bwatch_send_watch_found(struct command *cmd, send_outreq(req); } +/* Send a blockdepth depth notification to lightningd: same watch_found + * RPC shape but with depth + blockheight only (no tx). */ +void bwatch_send_blockdepth_found(struct command *cmd, + const struct watch *w, + u32 depth, + u32 blockheight) +{ + struct command *aux = aux_command(cmd); + struct out_req *req; + + req = jsonrpc_request_start(aux, "watch_found", + notify_ack, notify_ack, NULL); + json_add_u32(req->js, "blockheight", blockheight); + json_add_u32(req->js, "depth", depth); + + json_array_start(req->js, "owners"); + for (size_t i = 0; i < tal_count(w->owners); i++) + json_add_string(req->js, NULL, w->owners[i]); + json_array_end(req->js); + + send_outreq(req); +} + /* Tell one owner that a previously-reported watch_found was rolled back. */ void bwatch_send_watch_revert(struct command *cmd, const char *owner, diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h index 092d3f211ba8..7cea7c7b38ed 100644 --- a/plugins/bwatch/bwatch_interface.h +++ b/plugins/bwatch/bwatch_interface.h @@ -14,6 +14,12 @@ void bwatch_send_watch_found(struct command *cmd, u32 txindex, u32 index); +/* Send blockdepth depth notification to lightningd (no tx, just depth + height) */ +void bwatch_send_blockdepth_found(struct command *cmd, + const struct watch *w, + u32 depth, + u32 blockheight); + void bwatch_send_watch_revert(struct command *cmd, const char *owner, u32 blockheight); diff --git a/plugins/bwatch/bwatch_scanner.c b/plugins/bwatch/bwatch_scanner.c index 3dc13d9691b9..6f268c960188 100644 --- a/plugins/bwatch/bwatch_scanner.c +++ b/plugins/bwatch/bwatch_scanner.c @@ -161,3 +161,26 @@ void bwatch_process_block_txs(struct command *cmd, check_scid_watches(cmd, bwatch, block, blockheight); } + +/* Fire depth notifications for every active blockdepth watch. + * A watch with start_block > new_height is stale: its confirming block + * was reorged away, watch_revert has been sent, but the del hasn't + * arrived yet — skip it until deletion clears it from the table. */ +void bwatch_check_blockdepth_watches(struct command *cmd, + struct bwatch *bwatch, + u32 new_height) +{ + struct blockdepth_watches_iter it; + struct watch *w; + + /* We only have one per channel or so in practice, so don't optimize */ + for (w = blockdepth_watches_first(bwatch->blockdepth_watches, &it); + w; + w = blockdepth_watches_next(bwatch->blockdepth_watches, &it)) { + if (w->start_block > new_height) + continue; /* stale — awaiting deletion */ + + u32 depth = new_height - w->start_block + 1; + bwatch_send_blockdepth_found(cmd, w, depth, new_height); + } +} diff --git a/plugins/bwatch/bwatch_scanner.h b/plugins/bwatch/bwatch_scanner.h index 3d52c0639abb..4769d26c5cdb 100644 --- a/plugins/bwatch/bwatch_scanner.h +++ b/plugins/bwatch/bwatch_scanner.h @@ -12,4 +12,10 @@ void bwatch_process_block_txs(struct command *cmd, u32 blockheight, const struct bitcoin_blkid *blockhash); +/* Fire depth notifications for every active blockdepth watch at + * new_height. Called once per new block on the happy path. */ +void bwatch_check_blockdepth_watches(struct command *cmd, + struct bwatch *bwatch, + u32 new_height); + #endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_SCANNER_H */ From e60281c3c44799b2fd2b51ff1298d082f33a6a78 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:48:58 +0930 Subject: [PATCH 19/77] bwatch: send chaininfo to watchman on startup On init, query bcli for chain name, headercount, blockcount and IBD state, then forward the result to watchman via the chaininfo RPC before bwatch starts its normal poll loop. Watchman uses this to gate any work that depends on bitcoind being synced. If bitcoind's blockcount comes back lower than our persisted tip, peel stored blocks off until they line up so watchman gets a consistent picture. During steady-state polling the same case is handled by hash-mismatch reorg detection inside handle_block; this shortcut only matters at startup, before we've fetched anything. If bcli or watchman is not yet ready, log and fall back to scheduling the poll loop anyway so init never stalls. bwatch_remove_tip is exposed in bwatch.h so the chaininfo path in bwatch_interface.c can use it. --- plugins/bwatch/bwatch.c | 11 +-- plugins/bwatch/bwatch.h | 5 ++ plugins/bwatch/bwatch_interface.c | 115 ++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_interface.h | 5 ++ 4 files changed, 131 insertions(+), 5 deletions(-) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 1b31f98b98ec..9228c75841e1 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -79,8 +79,8 @@ static struct command_result *poll_finished(struct command *cmd) return timer_complete(cmd); } -/* Remove tip block on reorg. */ -static void bwatch_remove_tip(struct command *cmd, struct bwatch *bwatch) +/* Remove tip block on reorg */ +void bwatch_remove_tip(struct command *cmd, struct bwatch *bwatch) { const struct block_record_wire *newtip; size_t count = tal_count(bwatch->block_history); @@ -280,9 +280,10 @@ 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); + /* Send chaininfo to watchman first; the ack/err callbacks then + * kick off the chain-poll loop. */ + global_timer(cmd->plugin, time_from_sec(0), + bwatch_send_chaininfo, NULL); return NULL; } diff --git a/plugins/bwatch/bwatch.h b/plugins/bwatch/bwatch.h index 7a128ef50d6f..40fcf81e6079 100644 --- a/plugins/bwatch/bwatch.h +++ b/plugins/bwatch/bwatch.h @@ -83,4 +83,9 @@ struct bwatch *bwatch_of(struct plugin *plugin); * can schedule a poll from their own callbacks. */ struct command_result *bwatch_poll_chain(struct command *cmd, void *unused); +/* Pop the current tip from in-memory + persisted history. Exposed so the + * startup chaininfo path can roll back when bitcoind's chain is shorter + * than what we have stored. */ +void bwatch_remove_tip(struct command *cmd, struct bwatch *bwatch); + #endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_H */ diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index a1be223a3bfe..0ecc12d054db 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -187,3 +187,118 @@ void bwatch_send_revert_block_processed(struct command *cmd, u32 new_height, fmt_bitcoin_blkid(tmpctx, new_hash)); send_outreq(req); } + +/* + * ============================================================================ + * CHAININFO ON STARTUP + * + * On init bwatch first asks bcli for chain name / IBD state / current + * blockcount, optionally rolls its tip back if bitcoind is shorter than + * what we have on disk, and forwards the result to watchman via the + * `chaininfo` RPC. Whether watchman acks or errors, we then schedule + * the normal chain-poll loop. + * ============================================================================ + */ + +/* Watchman acked chaininfo: kick off normal polling. */ +static struct command_result *chaininfo_ack(struct command *cmd, + const char *method UNUSED, + const char *buf UNUSED, + const jsmntok_t *result UNUSED, + void *unused UNUSED) +{ + 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); +} + +/* Non-fatal: watchman may not be ready yet; poll anyway. */ +static struct command_result *chaininfo_err(struct command *cmd, + const char *method UNUSED, + const char *buf, + const jsmntok_t *result, + void *unused UNUSED) +{ + plugin_log(cmd->plugin, LOG_BROKEN, + "chaininfo RPC failed: %.*s", + json_tok_full_len(result), json_tok_full(buf, result)); + return chaininfo_ack(cmd, method, buf, result, unused); +} + +/* Got chain state from bcli: optionally roll back, then forward to watchman. */ +static struct command_result *chaininfo_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); + struct out_req *req; + const char *chain; + u32 headercount, blockcount; + bool ibd; + const char *err; + + err = json_scan(tmpctx, buf, result, + "{chain:%,headercount:%,blockcount:%,ibd:%}", + JSON_SCAN_TAL(tmpctx, json_strdup, &chain), + JSON_SCAN(json_to_number, &headercount), + JSON_SCAN(json_to_number, &blockcount), + JSON_SCAN(json_to_bool, &ibd)); + if (err) { + plugin_log(cmd->plugin, LOG_BROKEN, + "getchaininfo parse failed: %s", err); + return timer_complete(cmd); + } + + /* Startup-only rollback: if bitcoind's chain is shorter than our + * stored tip, peel off stale blocks now. During normal polling the + * shorter-chain case is handled by hash-mismatch reorg detection + * inside handle_block. */ + if (blockcount < bwatch->current_height) { + plugin_log(cmd->plugin, LOG_INFORM, + "Startup: chain at %u but bwatch at %u; rolling back", + blockcount, bwatch->current_height); + while (bwatch->current_height > blockcount + && bwatch_last_block(bwatch)) + bwatch_remove_tip(cmd, bwatch); + } + + req = jsonrpc_request_start(cmd, "chaininfo", + chaininfo_ack, chaininfo_err, NULL); + json_add_string(req->js, "chain", chain); + json_add_u32(req->js, "headercount", headercount); + json_add_u32(req->js, "blockcount", blockcount); + json_add_bool(req->js, "ibd", ibd); + return send_outreq(req); +} + +/* bcli unreachable: log and fall back to polling so we don't stall init. */ +static struct command_result *chaininfo_getchaininfo_failed(struct command *cmd, + const char *method UNUSED, + const char *buf UNUSED, + const jsmntok_t *result UNUSED, + void *unused UNUSED) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + plugin_log(cmd->plugin, LOG_BROKEN, + "getchaininfo failed during chaininfo init"); + bwatch->poll_timer = global_timer(cmd->plugin, time_from_sec(0), + bwatch_poll_chain, NULL); + return timer_complete(cmd); +} + +struct command_result *bwatch_send_chaininfo(struct command *cmd, + void *unused UNUSED) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + struct out_req *req; + + req = jsonrpc_request_start(cmd, "getchaininfo", + chaininfo_getchaininfo_done, + chaininfo_getchaininfo_failed, + NULL); + json_add_u32(req->js, "last_height", bwatch->current_height); + return send_outreq(req); +} diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h index 7cea7c7b38ed..8e946cfe0d8f 100644 --- a/plugins/bwatch/bwatch_interface.h +++ b/plugins/bwatch/bwatch_interface.h @@ -24,6 +24,11 @@ void bwatch_send_watch_revert(struct command *cmd, const char *owner, u32 blockheight); +/* Send chain name / IBD status / sync info to watchman on startup. + * Used as a timer callback from init; the ack/err handlers kick the + * normal chain-poll loop afterwards. */ +struct command_result *bwatch_send_chaininfo(struct command *cmd, void *unused); + /* Send a block_processed RPC to watchman after a new block has been * persisted. The next poll is started from the ack callback so we don't * race ahead of watchman's view of the chain. Chains on the same poll From 382b412c782a856e3134c2778ad5940c86375942 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:49:05 +0930 Subject: [PATCH 20/77] bwatch: add scriptpubkey watch RPCs addscriptpubkeywatch and delscriptpubkeywatch are how lightningd asks bwatch to start/stop watching an output script for a given owner. --- plugins/bwatch/bwatch.c | 3 +- plugins/bwatch/bwatch_interface.c | 57 +++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_interface.h | 8 +++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 9228c75841e1..6b8cb25ed7a5 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -288,7 +288,8 @@ static const char *init(struct command *cmd, } static const struct plugin_command commands[] = { - /* Subsequent commits register addwatch / delwatch / listwatch here. */ + { "addscriptpubkeywatch", json_bwatch_add_scriptpubkey }, + { "delscriptpubkeywatch", json_bwatch_del_scriptpubkey }, }; int main(int argc, char *argv[]) diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index 0ecc12d054db..61cab252ebc2 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -3,6 +3,7 @@ #include #include #include +#include /* * ============================================================================ @@ -302,3 +303,59 @@ struct command_result *bwatch_send_chaininfo(struct command *cmd, json_add_u32(req->js, "last_height", bwatch->current_height); return send_outreq(req); } + +/* + * ============================================================================ + * RPC COMMAND HANDLERS + * + * Watch RPCs are thin wrappers over bwatch_add_watch / bwatch_del_watch. + * Adding a watch with start_block <= current_height needs a historical + * rescan; the helper for that lands in a later commit. + * ============================================================================ + */ + +/* Register a scriptpubkey watch for `owner` from `start_block` onwards. */ +struct command_result *json_bwatch_add_scriptpubkey(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + const char *owner; + u8 *scriptpubkey; + u32 *start_block; + + if (!param(cmd, buffer, params, + p_req("owner", param_string, &owner), + p_req("scriptpubkey", param_bin_from_hex, &scriptpubkey), + p_req("start_block", param_u32, &start_block), + NULL)) + return command_param_failed(); + + /* New owner is appended to the watch's owner list; same owner + * re-adding lowers start_block if needed (rescan handled later). */ + bwatch_add_watch(cmd, bwatch, WATCH_SCRIPTPUBKEY, + NULL, scriptpubkey, NULL, NULL, + *start_block, owner); + return command_success(cmd, json_out_obj(cmd, NULL, NULL)); +} + +/* Drop one owner from a scriptpubkey watch; the watch itself goes away + * once the last owner is removed. */ +struct command_result *json_bwatch_del_scriptpubkey(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + const char *owner; + u8 *scriptpubkey; + + if (!param(cmd, buffer, params, + p_req("owner", param_string, &owner), + p_req("scriptpubkey", param_bin_from_hex, &scriptpubkey), + NULL)) + return command_param_failed(); + + bwatch_del_watch(cmd, bwatch, WATCH_SCRIPTPUBKEY, + NULL, scriptpubkey, NULL, NULL, owner); + return command_success(cmd, json_out_obj(cmd, "removed", "true")); +} diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h index 8e946cfe0d8f..8d7053124c5a 100644 --- a/plugins/bwatch/bwatch_interface.h +++ b/plugins/bwatch/bwatch_interface.h @@ -29,6 +29,14 @@ void bwatch_send_watch_revert(struct command *cmd, * normal chain-poll loop afterwards. */ struct command_result *bwatch_send_chaininfo(struct command *cmd, void *unused); +/* RPC handlers: add / remove a scriptpubkey watch. */ +struct command_result *json_bwatch_add_scriptpubkey(struct command *cmd, + const char *buffer, + const jsmntok_t *params); +struct command_result *json_bwatch_del_scriptpubkey(struct command *cmd, + const char *buffer, + const jsmntok_t *params); + /* Send a block_processed RPC to watchman after a new block has been * persisted. The next poll is started from the ack callback so we don't * race ahead of watchman's view of the chain. Chains on the same poll From 9a53b7cc6509e450d55c4c38812125da5cd16e28 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:49:10 +0930 Subject: [PATCH 21/77] bwatch: add outpoint watch RPCs addoutpointwatch and deloutpointwatch are how lightningd asks bwatch to start/stop watching a specific (txid, outnum) for a given owner. --- plugins/bwatch/bwatch.c | 2 ++ plugins/bwatch/bwatch_interface.c | 47 +++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_interface.h | 8 ++++++ 3 files changed, 57 insertions(+) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 6b8cb25ed7a5..d05124745369 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -289,7 +289,9 @@ static const char *init(struct command *cmd, static const struct plugin_command commands[] = { { "addscriptpubkeywatch", json_bwatch_add_scriptpubkey }, + { "addoutpointwatch", json_bwatch_add_outpoint }, { "delscriptpubkeywatch", json_bwatch_del_scriptpubkey }, + { "deloutpointwatch", json_bwatch_del_outpoint }, }; int main(int argc, char *argv[]) diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index 61cab252ebc2..5d1ec725c074 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -359,3 +359,50 @@ struct command_result *json_bwatch_del_scriptpubkey(struct command *cmd, NULL, scriptpubkey, NULL, NULL, owner); return command_success(cmd, json_out_obj(cmd, "removed", "true")); } + +/* Register an outpoint (txid + outnum) watch for `owner` from + * `start_block` onwards. */ +struct command_result *json_bwatch_add_outpoint(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + const char *owner; + struct bitcoin_outpoint *outpoint; + u32 *start_block; + + if (!param(cmd, buffer, params, + p_req("owner", param_string, &owner), + p_req("outpoint", param_outpoint, &outpoint), + p_req("start_block", param_u32, &start_block), + NULL)) + return command_param_failed(); + + /* New owner is appended to the watch's owner list; same owner + * re-adding lowers start_block if needed (rescan handled later). */ + bwatch_add_watch(cmd, bwatch, WATCH_OUTPOINT, + outpoint, NULL, NULL, NULL, + *start_block, owner); + return command_success(cmd, json_out_obj(cmd, NULL, NULL)); +} + +/* Drop one owner from an outpoint watch; the watch itself goes away + * once the last owner is removed. */ +struct command_result *json_bwatch_del_outpoint(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + const char *owner; + struct bitcoin_outpoint *outpoint; + + if (!param(cmd, buffer, params, + p_req("owner", param_string, &owner), + p_req("outpoint", param_outpoint, &outpoint), + NULL)) + return command_param_failed(); + + bwatch_del_watch(cmd, bwatch, WATCH_OUTPOINT, + outpoint, NULL, NULL, NULL, owner); + return command_success(cmd, json_out_obj(cmd, "removed", "true")); +} diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h index 8d7053124c5a..c947741389c4 100644 --- a/plugins/bwatch/bwatch_interface.h +++ b/plugins/bwatch/bwatch_interface.h @@ -37,6 +37,14 @@ struct command_result *json_bwatch_del_scriptpubkey(struct command *cmd, const char *buffer, const jsmntok_t *params); +/* RPC handlers: add / remove an outpoint watch. */ +struct command_result *json_bwatch_add_outpoint(struct command *cmd, + const char *buffer, + const jsmntok_t *params); +struct command_result *json_bwatch_del_outpoint(struct command *cmd, + const char *buffer, + const jsmntok_t *params); + /* Send a block_processed RPC to watchman after a new block has been * persisted. The next poll is started from the ack callback so we don't * race ahead of watchman's view of the chain. Chains on the same poll From 890e07d12253ca3bc69c10200324ed75e036f7c4 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:49:16 +0930 Subject: [PATCH 22/77] bwatch: add scid watch RPCs addscidwatch and delscidwatch are how lightningd asks bwatch to start/stop watching a specific short_channel_id for a given owner. The scid pins the watch to one (block, txindex, outnum), so on each new block we go straight to that position rather than scanning. --- plugins/bwatch/bwatch.c | 2 ++ plugins/bwatch/bwatch_interface.c | 48 +++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_interface.h | 8 ++++++ 3 files changed, 58 insertions(+) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index d05124745369..fdb8fe769bee 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -290,8 +290,10 @@ static const char *init(struct command *cmd, static const struct plugin_command commands[] = { { "addscriptpubkeywatch", json_bwatch_add_scriptpubkey }, { "addoutpointwatch", json_bwatch_add_outpoint }, + { "addscidwatch", json_bwatch_add_scid }, { "delscriptpubkeywatch", json_bwatch_del_scriptpubkey }, { "deloutpointwatch", json_bwatch_del_outpoint }, + { "delscidwatch", json_bwatch_del_scid }, }; int main(int argc, char *argv[]) diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index 5d1ec725c074..f312f8c1fd0c 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -406,3 +406,51 @@ struct command_result *json_bwatch_del_outpoint(struct command *cmd, outpoint, NULL, NULL, NULL, owner); return command_success(cmd, json_out_obj(cmd, "removed", "true")); } + +/* Register a short_channel_id watch for `owner` from `start_block` + * onwards. The scid pins the watch to one specific (block, txindex, + * outnum). */ +struct command_result *json_bwatch_add_scid(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + const char *owner; + struct short_channel_id *scid; + u32 *start_block; + + if (!param(cmd, buffer, params, + p_req("owner", param_string, &owner), + p_req("scid", param_short_channel_id, &scid), + p_req("start_block", param_u32, &start_block), + NULL)) + return command_param_failed(); + + /* New owner is appended to the watch's owner list; same owner + * re-adding lowers start_block if needed (rescan handled later). */ + bwatch_add_watch(cmd, bwatch, WATCH_SCID, + NULL, NULL, scid, NULL, + *start_block, owner); + return command_success(cmd, json_out_obj(cmd, NULL, NULL)); +} + +/* Drop one owner from a scid watch; the watch itself goes away once + * the last owner is removed. */ +struct command_result *json_bwatch_del_scid(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + const char *owner; + struct short_channel_id *scid; + + if (!param(cmd, buffer, params, + p_req("owner", param_string, &owner), + p_req("scid", param_short_channel_id, &scid), + NULL)) + return command_param_failed(); + + bwatch_del_watch(cmd, bwatch, WATCH_SCID, + NULL, NULL, scid, NULL, owner); + return command_success(cmd, json_out_obj(cmd, "removed", "true")); +} diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h index c947741389c4..edd5085f2d60 100644 --- a/plugins/bwatch/bwatch_interface.h +++ b/plugins/bwatch/bwatch_interface.h @@ -45,6 +45,14 @@ struct command_result *json_bwatch_del_outpoint(struct command *cmd, const char *buffer, const jsmntok_t *params); +/* RPC handlers: add / remove a scid watch. */ +struct command_result *json_bwatch_add_scid(struct command *cmd, + const char *buffer, + const jsmntok_t *params); +struct command_result *json_bwatch_del_scid(struct command *cmd, + const char *buffer, + const jsmntok_t *params); + /* Send a block_processed RPC to watchman after a new block has been * persisted. The next poll is started from the ack callback so we don't * race ahead of watchman's view of the chain. Chains on the same poll From 9e09a6650e238af656f5873049f5c7132416355f Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:50:15 +0930 Subject: [PATCH 23/77] bwatch: add blockdepth watch RPCs addblockdepthwatch and delblockdepthwatch are how lightningd asks bwatch to start/stop a depth-tracker for a given (owner, start_block). start_block doubles as the watch key and the anchor used to compute depth = tip - start_block + 1 on every new block. --- plugins/bwatch/bwatch.c | 2 ++ plugins/bwatch/bwatch_interface.c | 45 +++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_interface.h | 8 ++++++ 3 files changed, 55 insertions(+) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index fdb8fe769bee..7f37212afe59 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -291,9 +291,11 @@ static const struct plugin_command commands[] = { { "addscriptpubkeywatch", json_bwatch_add_scriptpubkey }, { "addoutpointwatch", json_bwatch_add_outpoint }, { "addscidwatch", json_bwatch_add_scid }, + { "addblockdepthwatch", json_bwatch_add_blockdepth }, { "delscriptpubkeywatch", json_bwatch_del_scriptpubkey }, { "deloutpointwatch", json_bwatch_del_outpoint }, { "delscidwatch", json_bwatch_del_scid }, + { "delblockdepthwatch", json_bwatch_del_blockdepth }, }; int main(int argc, char *argv[]) diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index f312f8c1fd0c..d3cecf967343 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -454,3 +454,48 @@ struct command_result *json_bwatch_del_scid(struct command *cmd, NULL, NULL, scid, NULL, owner); return command_success(cmd, json_out_obj(cmd, "removed", "true")); } + +/* Register a blockdepth watch for `owner` anchored at `start_block`. + * Each new block fires a watch_found with depth = tip - start_block + 1. */ +struct command_result *json_bwatch_add_blockdepth(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + const char *owner; + u32 *start_block; + + if (!param(cmd, buffer, params, + p_req("owner", param_string, &owner), + p_req("start_block", param_u32, &start_block), + NULL)) + return command_param_failed(); + + /* start_block doubles as the watch key (confirm_height) and + * the anchor for depth = tip - start_block + 1. */ + bwatch_add_watch(cmd, bwatch, WATCH_BLOCKDEPTH, + NULL, NULL, NULL, start_block, + *start_block, owner); + return command_success(cmd, json_out_obj(cmd, NULL, NULL)); +} + +/* Drop one owner from a blockdepth watch; the watch itself goes away + * once the last owner is removed. */ +struct command_result *json_bwatch_del_blockdepth(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + const char *owner; + u32 *start_block; + + if (!param(cmd, buffer, params, + p_req("owner", param_string, &owner), + p_req("start_block", param_u32, &start_block), + NULL)) + return command_param_failed(); + + bwatch_del_watch(cmd, bwatch, WATCH_BLOCKDEPTH, + NULL, NULL, NULL, start_block, owner); + return command_success(cmd, json_out_obj(cmd, "removed", "true")); +} diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h index edd5085f2d60..5e5ec0c6e9cd 100644 --- a/plugins/bwatch/bwatch_interface.h +++ b/plugins/bwatch/bwatch_interface.h @@ -53,6 +53,14 @@ struct command_result *json_bwatch_del_scid(struct command *cmd, const char *buffer, const jsmntok_t *params); +/* RPC handlers: add / remove a blockdepth watch. */ +struct command_result *json_bwatch_add_blockdepth(struct command *cmd, + const char *buffer, + const jsmntok_t *params); +struct command_result *json_bwatch_del_blockdepth(struct command *cmd, + const char *buffer, + const jsmntok_t *params); + /* Send a block_processed RPC to watchman after a new block has been * persisted. The next poll is started from the ack callback so we don't * race ahead of watchman's view of the chain. Chains on the same poll From ff0a7c1704603eb8b6e43cef79d494d1af7ce192 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:51:27 +0930 Subject: [PATCH 24/77] bwatch: add listwatch RPC listwatch returns every active watch as a flat array. Each entry carries its type-specific key (scriptpubkey hex, outpoint, scid triple, or blockdepth anchor) plus the common type / start_block / owners fields, so callers can dispatch on the per-type key without parsing the type string first. Mostly used by tests and operator tooling to inspect what bwatch is currently tracking. --- plugins/bwatch/bwatch.c | 1 + plugins/bwatch/bwatch_interface.c | 85 +++++++++++++++++++++++++++++++ plugins/bwatch/bwatch_interface.h | 5 ++ 3 files changed, 91 insertions(+) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 7f37212afe59..9d5571fe098c 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -296,6 +296,7 @@ static const struct plugin_command commands[] = { { "deloutpointwatch", json_bwatch_del_outpoint }, { "delscidwatch", json_bwatch_del_scid }, { "delblockdepthwatch", json_bwatch_del_blockdepth }, + { "listwatch", json_bwatch_list }, }; int main(int argc, char *argv[]) diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index d3cecf967343..34bd5d179e67 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -1,4 +1,5 @@ #include "config.h" +#include #include #include #include @@ -499,3 +500,87 @@ struct command_result *json_bwatch_del_blockdepth(struct command *cmd, NULL, NULL, NULL, start_block, owner); return command_success(cmd, json_out_obj(cmd, "removed", "true")); } + +/* Emit type / start_block / owners for one watch. */ +static void json_out_watch_common(struct json_out *jout, + enum watch_type type, + u32 start_block, + wirestring **owners) +{ + json_out_addstr(jout, "type", bwatch_get_watch_type_name(type)); + json_out_add(jout, "start_block", false, "%u", start_block); + json_out_start(jout, "owners", '['); + for (size_t i = 0; i < tal_count(owners); i++) + json_out_addstr(jout, NULL, owners[i]); + json_out_end(jout, ']'); +} + +/* Dump every active watch as a flat array; per-type fields go first + * so the consumer can dispatch on shape. */ +struct command_result *json_bwatch_list(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct bwatch *bwatch = bwatch_of(cmd->plugin); + struct json_out *jout; + struct watch *w; + struct scriptpubkey_watches_iter sit; + struct outpoint_watches_iter oit; + struct scid_watches_iter scit; + struct blockdepth_watches_iter bdit; + + if (!param(cmd, buffer, params, NULL)) + return command_param_failed(); + + jout = json_out_new(cmd); + json_out_start(jout, NULL, '{'); + json_out_start(jout, "watches", '['); + + for (w = scriptpubkey_watches_first(bwatch->scriptpubkey_watches, &sit); + w; + w = scriptpubkey_watches_next(bwatch->scriptpubkey_watches, &sit)) { + json_out_start(jout, NULL, '{'); + json_out_addstr(jout, "scriptpubkey", + tal_hexstr(tmpctx, w->key.scriptpubkey.script, + w->key.scriptpubkey.len)); + json_out_watch_common(jout, w->type, w->start_block, w->owners); + json_out_end(jout, '}'); + } + + for (w = outpoint_watches_first(bwatch->outpoint_watches, &oit); + w; + w = outpoint_watches_next(bwatch->outpoint_watches, &oit)) { + json_out_start(jout, NULL, '{'); + json_out_addstr(jout, "outpoint", + fmt_bitcoin_outpoint(tmpctx, &w->key.outpoint)); + json_out_watch_common(jout, w->type, w->start_block, w->owners); + json_out_end(jout, '}'); + } + + for (w = scid_watches_first(bwatch->scid_watches, &scit); + w; + w = scid_watches_next(bwatch->scid_watches, &scit)) { + json_out_start(jout, NULL, '{'); + json_out_add(jout, "blockheight", false, "%u", + short_channel_id_blocknum(w->key.scid)); + json_out_add(jout, "txindex", false, "%u", + short_channel_id_txnum(w->key.scid)); + json_out_add(jout, "outnum", false, "%u", + short_channel_id_outnum(w->key.scid)); + json_out_watch_common(jout, w->type, w->start_block, w->owners); + json_out_end(jout, '}'); + } + + for (w = blockdepth_watches_first(bwatch->blockdepth_watches, &bdit); + w; + w = blockdepth_watches_next(bwatch->blockdepth_watches, &bdit)) { + json_out_start(jout, NULL, '{'); + json_out_add(jout, "blockdepth", false, "%u", w->start_block); + json_out_watch_common(jout, w->type, w->start_block, w->owners); + json_out_end(jout, '}'); + } + + json_out_end(jout, ']'); + json_out_end(jout, '}'); + return command_success(cmd, jout); +} diff --git a/plugins/bwatch/bwatch_interface.h b/plugins/bwatch/bwatch_interface.h index 5e5ec0c6e9cd..1e9543588bd1 100644 --- a/plugins/bwatch/bwatch_interface.h +++ b/plugins/bwatch/bwatch_interface.h @@ -61,6 +61,11 @@ struct command_result *json_bwatch_del_blockdepth(struct command *cmd, const char *buffer, const jsmntok_t *params); +/* RPC handler: dump every active watch. */ +struct command_result *json_bwatch_list(struct command *cmd, + const char *buffer, + const jsmntok_t *params); + /* Send a block_processed RPC to watchman after a new block has been * persisted. The next poll is started from the ack callback so we don't * race ahead of watchman's view of the chain. Chains on the same poll From 164c00a03ff7f84034d41d086e85eae8239fe97f Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:54:11 +0930 Subject: [PATCH 25/77] bwatch: thread per-watch parameter through block scanning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To support rescans (added next), bwatch_process_block_txs and bwatch_check_scid_watches gain a `const struct watch *w` parameter so the caller can ask the scanner to check just one watch instead of all of them. When a new watch is added with start_block <= current_height (say the watch starts at block 100 but bwatch is already at 105) we need to replay blocks 100..105 for that watch alone — not re-scan every active watch over those blocks. w == NULL -> check every active watch (normal polling) w != NULL -> check only that one watch (rescan) --- plugins/bwatch/bwatch.c | 2 +- plugins/bwatch/bwatch_scanner.c | 97 ++++++++++++++++++++++++++++++--- plugins/bwatch/bwatch_scanner.h | 18 +++++- 3 files changed, 104 insertions(+), 13 deletions(-) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index 9d5571fe098c..b5fd72754fac 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -168,7 +168,7 @@ static struct command_result *handle_block(struct command *cmd, * fire for the same block. */ bwatch_check_blockdepth_watches(cmd, bwatch, block_height); bwatch_process_block_txs(cmd, bwatch, block, block_height, - &blockhash); + &blockhash, NULL); } /* Update state */ diff --git a/plugins/bwatch/bwatch_scanner.c b/plugins/bwatch/bwatch_scanner.c index 6f268c960188..9317be45b9b9 100644 --- a/plugins/bwatch/bwatch_scanner.c +++ b/plugins/bwatch/bwatch_scanner.c @@ -1,5 +1,6 @@ #include "config.h" #include +#include #include #include #include @@ -90,6 +91,74 @@ static void check_tx_against_all_watches(struct command *cmd, check_outpoint_watches(cmd, bwatch, tx, blockheight, blockhash, txindex); } +/* Check tx outputs against a single scriptpubkey watch (rescan path). */ +static void check_tx_scriptpubkey(struct command *cmd, + const struct bitcoin_tx *tx, + const struct watch *w, + u32 blockheight, + const struct bitcoin_blkid *blockhash, + u32 txindex) +{ + for (size_t i = 0; i < tx->wtx->num_outputs; i++) { + if (memeq(tx->wtx->outputs[i].script, + tx->wtx->outputs[i].script_len, + w->key.scriptpubkey.script, + w->key.scriptpubkey.len)) { + bwatch_send_watch_found(cmd, tx, blockheight, w, + txindex, i); + /* Same scriptpubkey may appear in multiple outputs. */ + } + } +} + +/* Check tx inputs against a single outpoint watch (rescan path). */ +static void check_tx_outpoint(struct command *cmd, + const struct bitcoin_tx *tx, + const struct watch *w, + u32 blockheight, + const struct bitcoin_blkid *blockhash, + u32 txindex) +{ + for (size_t i = 0; i < tx->wtx->num_inputs; i++) { + struct bitcoin_outpoint outpoint; + + bitcoin_tx_input_get_txid(tx, i, &outpoint.txid); + outpoint.n = tx->wtx->inputs[i].index; + + if (bitcoin_outpoint_eq(&outpoint, &w->key.outpoint)) { + bwatch_send_watch_found(cmd, tx, blockheight, w, + txindex, i); + return; /* an outpoint can only be spent once */ + } + } +} + +/* Dispatch a single watch against one tx (rescan path). */ +static void check_tx_for_single_watch(struct command *cmd, + const struct watch *w, + const struct bitcoin_tx *tx, + u32 blockheight, + const struct bitcoin_blkid *blockhash, + u32 txindex) +{ + switch (w->type) { + case WATCH_SCRIPTPUBKEY: + check_tx_scriptpubkey(cmd, tx, w, blockheight, blockhash, txindex); + break; + case WATCH_OUTPOINT: + check_tx_outpoint(cmd, tx, w, blockheight, blockhash, txindex); + break; + case WATCH_SCID: + /* scid watches don't scan transactions: txindex is encoded in + * the scid key, so bwatch_check_scid_watches handles them + * directly at the block level. */ + break; + case WATCH_BLOCKDEPTH: + /* blockdepth watches fire per block; no per-tx work. */ + break; + } +} + /* Fire watch_found for a scid watch anchored to this block. */ static void maybe_fire_scid_watch(struct command *cmd, const struct bitcoin_block *block, @@ -132,13 +201,17 @@ static void maybe_fire_scid_watch(struct command *cmd, bwatch_send_watch_found(cmd, tx, blockheight, w, txindex, outnum); } -/* Walk every scid watch and fire watch_found for any whose encoded - * blockheight matches this block. */ -static void check_scid_watches(struct command *cmd, +void bwatch_check_scid_watches(struct command *cmd, struct bwatch *bwatch, const struct bitcoin_block *block, - u32 blockheight) + u32 blockheight, + const struct watch *w) { + if (w) { + maybe_fire_scid_watch(cmd, block, blockheight, w); + return; + } + struct scid_watches_iter it; struct watch *scid_w; @@ -153,13 +226,19 @@ void bwatch_process_block_txs(struct command *cmd, struct bwatch *bwatch, const struct bitcoin_block *block, u32 blockheight, - const struct bitcoin_blkid *blockhash) + const struct bitcoin_blkid *blockhash, + const struct watch *w) { - for (size_t i = 0; i < tal_count(block->tx); i++) - check_tx_against_all_watches(cmd, bwatch, block->tx[i], - blockheight, blockhash, i); + for (size_t i = 0; i < tal_count(block->tx); i++) { + if (w) + check_tx_for_single_watch(cmd, w, block->tx[i], + blockheight, blockhash, i); + else + check_tx_against_all_watches(cmd, bwatch, block->tx[i], + blockheight, blockhash, i); + } - check_scid_watches(cmd, bwatch, block, blockheight); + bwatch_check_scid_watches(cmd, bwatch, block, blockheight, w); } /* Fire depth notifications for every active blockdepth watch. diff --git a/plugins/bwatch/bwatch_scanner.h b/plugins/bwatch/bwatch_scanner.h index 4769d26c5cdb..a8a81f6f9543 100644 --- a/plugins/bwatch/bwatch_scanner.h +++ b/plugins/bwatch/bwatch_scanner.h @@ -4,13 +4,25 @@ #include "config.h" #include -/* Scan every transaction in a block against the active scriptpubkey - * and outpoint watches, firing watch_found for each match. */ +/* Scan a block against scriptpubkey and outpoint watches, firing + * watch_found for each match. If `w` is NULL all active watches are + * checked (normal polling); if non-NULL only that watch is checked + * (single-watch rescan). */ void bwatch_process_block_txs(struct command *cmd, struct bwatch *bwatch, const struct bitcoin_block *block, u32 blockheight, - const struct bitcoin_blkid *blockhash); + const struct bitcoin_blkid *blockhash, + const struct watch *w); + +/* Fire watch_found for scid watches anchored to this block. + * w==NULL walks every scid watch (normal polling); w non-NULL + * fires only that watch (single-watch rescan). */ +void bwatch_check_scid_watches(struct command *cmd, + struct bwatch *bwatch, + const struct bitcoin_block *block, + u32 blockheight, + const struct watch *w); /* Fire depth notifications for every active blockdepth watch at * new_height. Called once per new block on the happy path. */ From d385f04689eae9c38cb38b4d14fb55be4121165f Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 14:54:14 +0930 Subject: [PATCH 26/77] bwatch: add rescan engine for historical blocks bwatch_start_rescan(cmd, w, start_block, target_block) replays blocks from start_block..target_block for a single watch w (or for all watches if w is NULL). The rescan runs asynchronously: fetch_block_rescan -> rescan_block_done -> next fetch, terminating with rescan_complete (which returns success for an RPC-driven rescan and aux_command_done for a timer-driven one). Nothing calls bwatch_start_rescan yet; the add-watch RPCs wire it up next. --- plugins/bwatch/bwatch.c | 114 ++++++++++++++++++++++++++++++++++++++++ plugins/bwatch/bwatch.h | 15 ++++++ 2 files changed, 129 insertions(+) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index b5fd72754fac..cd64aedd74ec 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -258,6 +258,120 @@ struct command_result *bwatch_poll_chain(struct command *cmd, return send_outreq(req); } +/* + * ============================================================================ + * RESCAN + * + * When a watch is added with start_block <= current_height, replay the + * historical blocks for that one watch so it sees confirmations that + * happened before it was registered. Bounded by current_height so we + * never race the live polling loop. + * + * Async chain: fetch_block_rescan -> rescan_block_done -> next fetch. + * ============================================================================ + */ + +/* Fetch a single block by height during a rescan. */ +static struct command_result *fetch_block_rescan(struct command *cmd, + u32 height, + struct command_result *(*cb)(struct command *, + const char *, + const char *, + const jsmntok_t *, + struct rescan_state *), + struct rescan_state *rescan) +{ + struct out_req *req = jsonrpc_request_start(cmd, "getrawblockbyheight", + cb, cb, rescan); + json_add_u32(req->js, "height", height); + return send_outreq(req); +} + +/* Finish a rescan chain: RPC commands get a JSON result; aux/timer + * commands just terminate. */ +static struct command_result *rescan_complete(struct command *cmd) +{ + switch (cmd->type) { + case COMMAND_TYPE_NORMAL: + case COMMAND_TYPE_HOOK: + return command_success(cmd, json_out_obj(cmd, NULL, NULL)); + case COMMAND_TYPE_AUX: + return aux_command_done(cmd); + case COMMAND_TYPE_NOTIFICATION: + case COMMAND_TYPE_TIMER: + case COMMAND_TYPE_CHECK: + case COMMAND_TYPE_USAGE_ONLY: + break; + } + abort(); +} + +/* getrawblockbyheight callback for one block of a rescan: process the + * block, then either fetch the next or finish. */ +static struct command_result *rescan_block_done(struct command *cmd, + const char *method UNUSED, + const char *buf, + const jsmntok_t *result, + struct rescan_state *rescan) +{ + struct bitcoin_blkid blockhash; + struct bitcoin_block *block = block_from_response(buf, result, &blockhash); + + if (!block) { + /* Chain may have rolled back past this height; stop quietly. */ + plugin_log(cmd->plugin, LOG_DBG, + "Rescan: block %u unavailable (chain rolled back?), stopping", + rescan->current_block); + return rescan_complete(cmd); + } + + /* rescan->watch is forwarded so the scanner only checks that one + * watch (or all watches when watch == NULL). */ + bwatch_process_block_txs(cmd, bwatch_of(cmd->plugin), block, + rescan->current_block, &blockhash, rescan->watch); + + /* Advance the cursor; if we still have blocks to scan, fetch the + * next one and chain back into rescan_block_done. */ + if (++rescan->current_block <= rescan->target_block) + return fetch_block_rescan(cmd, rescan->current_block, + rescan_block_done, rescan); + + plugin_log(cmd->plugin, LOG_INFORM, "Rescan complete"); + return rescan_complete(cmd); +} + +void bwatch_start_rescan(struct command *cmd, + const struct watch *w, + u32 start_block, + u32 target_block) +{ + struct rescan_state *rescan; + + if (w) { + plugin_log(cmd->plugin, LOG_INFORM, + "Starting rescan for %s watch: blocks %u-%u", + bwatch_get_watch_type_name(w->type), + start_block, target_block); + } else { + plugin_log(cmd->plugin, LOG_INFORM, + "Starting rescan for all watches: blocks %u-%u", + start_block, target_block); + } + + /* Owned by `cmd` so it lives across the async chain and gets + * freed automatically when the command completes. */ + rescan = tal(cmd, struct rescan_state); + rescan->watch = w; + rescan->current_block = start_block; + rescan->target_block = target_block; + + /* Fire the first getrawblockbyheight; each response runs + * rescan_block_done, which fetches the next block until we + * pass target_block. */ + fetch_block_rescan(cmd, rescan->current_block, + rescan_block_done, rescan); +} + static const char *init(struct command *cmd, const char *buf UNUSED, const jsmntok_t *config UNUSED) diff --git a/plugins/bwatch/bwatch.h b/plugins/bwatch/bwatch.h index 40fcf81e6079..ab60286a1552 100644 --- a/plugins/bwatch/bwatch.h +++ b/plugins/bwatch/bwatch.h @@ -88,4 +88,19 @@ struct command_result *bwatch_poll_chain(struct command *cmd, void *unused); * than what we have stored. */ void bwatch_remove_tip(struct command *cmd, struct bwatch *bwatch); +/* Per-rescan cursor: which block we're on and how far to go. */ +struct rescan_state { + const struct watch *watch; /* NULL = rescan all watches, non-NULL = single watch */ + u32 current_block; /* Next block to fetch */ + u32 target_block; /* Stop after this block */ +}; + +/* Replay historical blocks for `w` (or all watches if w==NULL) from + * `start_block` up to `target_block` inclusive. Runs asynchronously: + * fetch -> process -> fetch the next block. */ +void bwatch_start_rescan(struct command *cmd, + const struct watch *w, + u32 start_block, + u32 target_block); + #endif /* LIGHTNING_PLUGINS_BWATCH_BWATCH_H */ From 4ba6cfc45841837d9f4cac86527ecfb30115e8ad Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:16:58 +0930 Subject: [PATCH 27/77] bwatch: trigger rescan when a watch is added behind tip bwatch_add_watch returns the watch it created (or found); each addwatch RPC now passes that into add_watch_and_maybe_rescan, which: - returns success immediately if start_block > current_height (the watch only cares about future blocks), and - otherwise calls bwatch_start_rescan over [start_block, current_height] for that one watch and leaves the RPC pending until the rescan completes. This lets callers add a watch for an event that already confirmed (e.g. a channel funding tx some blocks back) and still get a watch_found. --- plugins/bwatch/bwatch_interface.c | 63 ++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/plugins/bwatch/bwatch_interface.c b/plugins/bwatch/bwatch_interface.c index 34bd5d179e67..fffa0a66e3c4 100644 --- a/plugins/bwatch/bwatch_interface.c +++ b/plugins/bwatch/bwatch_interface.c @@ -310,11 +310,28 @@ struct command_result *bwatch_send_chaininfo(struct command *cmd, * RPC COMMAND HANDLERS * * Watch RPCs are thin wrappers over bwatch_add_watch / bwatch_del_watch. - * Adding a watch with start_block <= current_height needs a historical - * rescan; the helper for that lands in a later commit. + * Adding a watch whose start_block is <= our current chain tip needs a + * historical rescan so it sees confirmations that happened before the + * watch was registered; add_watch_and_maybe_rescan handles that. * ============================================================================ */ +/* If this watch's start_block is at or behind our tip, replay the + * historical range for just this watch; otherwise we can return + * success immediately. */ +static struct command_result *add_watch_and_maybe_rescan(struct command *cmd, + struct bwatch *bwatch, + struct watch *w, + u32 scan_start) +{ + if (w && bwatch->current_height > 0 + && scan_start <= bwatch->current_height) { + bwatch_start_rescan(cmd, w, scan_start, bwatch->current_height); + return command_still_pending(cmd); + } + return command_success(cmd, json_out_obj(cmd, NULL, NULL)); +} + /* Register a scriptpubkey watch for `owner` from `start_block` onwards. */ struct command_result *json_bwatch_add_scriptpubkey(struct command *cmd, const char *buffer, @@ -324,6 +341,7 @@ struct command_result *json_bwatch_add_scriptpubkey(struct command *cmd, const char *owner; u8 *scriptpubkey; u32 *start_block; + struct watch *w; if (!param(cmd, buffer, params, p_req("owner", param_string, &owner), @@ -333,11 +351,11 @@ struct command_result *json_bwatch_add_scriptpubkey(struct command *cmd, return command_param_failed(); /* New owner is appended to the watch's owner list; same owner - * re-adding lowers start_block if needed (rescan handled later). */ - bwatch_add_watch(cmd, bwatch, WATCH_SCRIPTPUBKEY, - NULL, scriptpubkey, NULL, NULL, - *start_block, owner); - return command_success(cmd, json_out_obj(cmd, NULL, NULL)); + * re-adding lowers start_block if needed. */ + w = bwatch_add_watch(cmd, bwatch, WATCH_SCRIPTPUBKEY, + NULL, scriptpubkey, NULL, NULL, + *start_block, owner); + return add_watch_and_maybe_rescan(cmd, bwatch, w, *start_block); } /* Drop one owner from a scriptpubkey watch; the watch itself goes away @@ -371,6 +389,7 @@ struct command_result *json_bwatch_add_outpoint(struct command *cmd, const char *owner; struct bitcoin_outpoint *outpoint; u32 *start_block; + struct watch *w; if (!param(cmd, buffer, params, p_req("owner", param_string, &owner), @@ -380,11 +399,11 @@ struct command_result *json_bwatch_add_outpoint(struct command *cmd, return command_param_failed(); /* New owner is appended to the watch's owner list; same owner - * re-adding lowers start_block if needed (rescan handled later). */ - bwatch_add_watch(cmd, bwatch, WATCH_OUTPOINT, - outpoint, NULL, NULL, NULL, - *start_block, owner); - return command_success(cmd, json_out_obj(cmd, NULL, NULL)); + * re-adding lowers start_block if needed. */ + w = bwatch_add_watch(cmd, bwatch, WATCH_OUTPOINT, + outpoint, NULL, NULL, NULL, + *start_block, owner); + return add_watch_and_maybe_rescan(cmd, bwatch, w, *start_block); } /* Drop one owner from an outpoint watch; the watch itself goes away @@ -419,6 +438,7 @@ struct command_result *json_bwatch_add_scid(struct command *cmd, const char *owner; struct short_channel_id *scid; u32 *start_block; + struct watch *w; if (!param(cmd, buffer, params, p_req("owner", param_string, &owner), @@ -428,11 +448,11 @@ struct command_result *json_bwatch_add_scid(struct command *cmd, return command_param_failed(); /* New owner is appended to the watch's owner list; same owner - * re-adding lowers start_block if needed (rescan handled later). */ - bwatch_add_watch(cmd, bwatch, WATCH_SCID, - NULL, NULL, scid, NULL, - *start_block, owner); - return command_success(cmd, json_out_obj(cmd, NULL, NULL)); + * re-adding lowers start_block if needed. */ + w = bwatch_add_watch(cmd, bwatch, WATCH_SCID, + NULL, NULL, scid, NULL, + *start_block, owner); + return add_watch_and_maybe_rescan(cmd, bwatch, w, *start_block); } /* Drop one owner from a scid watch; the watch itself goes away once @@ -465,6 +485,7 @@ struct command_result *json_bwatch_add_blockdepth(struct command *cmd, struct bwatch *bwatch = bwatch_of(cmd->plugin); const char *owner; u32 *start_block; + struct watch *w; if (!param(cmd, buffer, params, p_req("owner", param_string, &owner), @@ -474,10 +495,10 @@ struct command_result *json_bwatch_add_blockdepth(struct command *cmd, /* start_block doubles as the watch key (confirm_height) and * the anchor for depth = tip - start_block + 1. */ - bwatch_add_watch(cmd, bwatch, WATCH_BLOCKDEPTH, - NULL, NULL, NULL, start_block, - *start_block, owner); - return command_success(cmd, json_out_obj(cmd, NULL, NULL)); + w = bwatch_add_watch(cmd, bwatch, WATCH_BLOCKDEPTH, + NULL, NULL, NULL, start_block, + *start_block, owner); + return add_watch_and_maybe_rescan(cmd, bwatch, w, *start_block); } /* Drop one owner from a blockdepth watch; the watch itself goes away From 10aff1bdd5aee54a9d5426e66844573cb129f86f Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 21:41:05 +0930 Subject: [PATCH 28/77] bwatch: notify watch owners on reorg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When bwatch removes its tip block on a reorg, fire watch_revert for the affected owners so lightningd-side handlers actually run. Two cases, depending on whether the watch has an anchor block: - scriptpubkey watches have no anchor (a wallet address can receive funds in any block), so notify every owner on every removed block. Handlers are cheap and defensive — they check their own state and no-op if there is nothing to undo. - outpoint, scid, and blockdepth watches each carry a start_block. Notify only those with start_block >= removed_height (the watch's anchor is gone). Older watches stay armed and refire naturally on the new chain. Owners are snapshotted before dispatch so revert handlers can safely call watchman_unwatch_* and mutate the watch tables. --- plugins/bwatch/bwatch.c | 61 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/plugins/bwatch/bwatch.c b/plugins/bwatch/bwatch.c index cd64aedd74ec..4e5486089475 100644 --- a/plugins/bwatch/bwatch.c +++ b/plugins/bwatch/bwatch.c @@ -79,6 +79,63 @@ static struct command_result *poll_finished(struct command *cmd) return timer_complete(cmd); } +/* Send watch_revert for every owner affected by losing @removed_height. */ +static void bwatch_notify_reorg_watches(struct command *cmd, + struct bwatch *bwatch, + u32 removed_height) +{ + const char **owners = tal_arr(tmpctx, const char *, 0); + struct watch *w; + + /* Snapshot owners first; revert handlers may call watchman_del and + * mutate these tables. */ + + /* Scriptpubkey watches are perennial: always notify. */ + struct scriptpubkey_watches_iter sit; + for (w = scriptpubkey_watches_first(bwatch->scriptpubkey_watches, &sit); + w; + w = scriptpubkey_watches_next(bwatch->scriptpubkey_watches, &sit)) { + for (size_t i = 0; i < tal_count(w->owners); i++) + tal_arr_expand(&owners, w->owners[i]); + } + + /* Outpoint/scid/blockdepth: only notify watches whose anchor block is + * being torn down (start_block >= removed_height). Older long-lived + * watches stay armed and will refire naturally on the new chain. */ + struct outpoint_watches_iter oit; + for (w = outpoint_watches_first(bwatch->outpoint_watches, &oit); + w; + w = outpoint_watches_next(bwatch->outpoint_watches, &oit)) { + if (w->start_block < removed_height) + continue; + for (size_t i = 0; i < tal_count(w->owners); i++) + tal_arr_expand(&owners, w->owners[i]); + } + + struct scid_watches_iter scit; + for (w = scid_watches_first(bwatch->scid_watches, &scit); + w; + w = scid_watches_next(bwatch->scid_watches, &scit)) { + if (w->start_block < removed_height) + continue; + for (size_t i = 0; i < tal_count(w->owners); i++) + tal_arr_expand(&owners, w->owners[i]); + } + + struct blockdepth_watches_iter bdit; + for (w = blockdepth_watches_first(bwatch->blockdepth_watches, &bdit); + w; + w = blockdepth_watches_next(bwatch->blockdepth_watches, &bdit)) { + if (w->start_block < removed_height) + continue; + for (size_t i = 0; i < tal_count(w->owners); i++) + tal_arr_expand(&owners, w->owners[i]); + } + + for (size_t i = 0; i < tal_count(owners); i++) + bwatch_send_watch_revert(cmd, owners[i], removed_height); +} + /* Remove tip block on reorg */ void bwatch_remove_tip(struct command *cmd, struct bwatch *bwatch) { @@ -95,6 +152,10 @@ void bwatch_remove_tip(struct command *cmd, struct bwatch *bwatch) bwatch->current_height, fmt_bitcoin_blkid(tmpctx, &bwatch->current_blockhash)); + /* Notify owners of any watch affected by losing this block before we + * tear it down, so they can roll back in the same order things happened. */ + bwatch_notify_reorg_watches(cmd, bwatch, bwatch->current_height); + /* Delete block from datastore */ bwatch_delete_block_from_datastore(cmd, bwatch->current_height); From 871813bf6a89cc07b1ba908bb4a64f7dea06f5b3 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:17:09 +0930 Subject: [PATCH 29/77] pyln-testing: shorten bwatch poll interval Default poll cadence is 30s; tests would otherwise wait that long between block_processed notifications. Drop to 500ms so block-by- block assertions don't sit idle. --- contrib/pyln-testing/pyln/testing/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/pyln-testing/pyln/testing/utils.py b/contrib/pyln-testing/pyln/testing/utils.py index 861afcea2c76..da6babcacb50 100644 --- a/contrib/pyln-testing/pyln/testing/utils.py +++ b/contrib/pyln-testing/pyln/testing/utils.py @@ -771,6 +771,7 @@ def __init__( self.opts['dev-fast-gossip'] = None self.opts['dev-bitcoind-poll'] = 1 + self.opts['bwatch-poll-interval'] = 500 # 0.5s for fast test feedback self.prefix = 'lightningd-%d' % (node_id) # Log to stdout so we see it in failure cases, and log file for TailableProc. self.opts['log-file'] = ['-', os.path.join(lightning_dir, "log")] From ff1098afd9d69185186859a7b800fcb3717d7637 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:17:09 +0930 Subject: [PATCH 30/77] lightningd: add watchman.h declarations watchman is the lightningd-side counterpart to the bwatch plugin landed in the previous group: it tracks how far we've processed the chain, queues outbound watch ops while bwatch is starting up, and dispatches watch_found/watch_revert/blockdepth notifications to subdaemon-specific handlers. This commit adds only the public surface (struct watchman, the three handler typedefs, and prototypes for watchman_new, watchman_ack, watchman_replay_pending) plus an empty watchman.c so the header is exercised by the build. Definitions land in subsequent commits. --- lightningd/Makefile | 1 + lightningd/watchman.c | 5 +++ lightningd/watchman.h | 87 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 lightningd/watchman.c create mode 100644 lightningd/watchman.h diff --git a/lightningd/Makefile b/lightningd/Makefile index 4fb5afbc269c..2d6950d39d7d 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -4,6 +4,7 @@ LIGHTNINGD_SRC := \ lightningd/anchorspend.c \ lightningd/bitcoind.c \ lightningd/chaintopology.c \ + lightningd/watchman.c \ lightningd/channel.c \ lightningd/channel_control.c \ lightningd/channel_gossip.c \ diff --git a/lightningd/watchman.c b/lightningd/watchman.c new file mode 100644 index 000000000000..9a1ed50a8d50 --- /dev/null +++ b/lightningd/watchman.c @@ -0,0 +1,5 @@ +#include "config.h" +#include + +/* Definitions land in subsequent commits (storage skeleton, watchman_new, + * send_to_bwatch, ack handling, enqueue/replay). */ diff --git a/lightningd/watchman.h b/lightningd/watchman.h new file mode 100644 index 000000000000..c06671a7b45a --- /dev/null +++ b/lightningd/watchman.h @@ -0,0 +1,87 @@ +#ifndef LIGHTNING_LIGHTNINGD_WATCHMAN_H +#define LIGHTNING_LIGHTNINGD_WATCHMAN_H + +#include "config.h" +#include +#include + +struct lightningd; +struct pending_op; + +/* lightningd's view of bwatch. bwatch lives in a separate process and tells + * us about new/reverted blocks and watch hits via JSON-RPC; watchman tracks + * what we've already processed and queues outbound watch ops while bwatch is + * starting up. */ +struct watchman { + struct lightningd *ld; + u32 last_processed_height; + struct bitcoin_blkid last_processed_hash; + u32 bitcoind_blockcount; + struct pending_op **pending_ops; +}; + +/** + * watch_found_fn - Handler for watch_found notifications (tx-based watches) + * @ld: lightningd instance + * @suffix: the owner string after the prefix (e.g. "42" for wallet/p2wpkh/42, + * or "100x1x0" for gossip/100x1x0); the handler is responsible for + * parsing whatever identifier it stored in that suffix + * @tx: the transaction that matched + * @outnum: which output matched (for scriptpubkey watches) or input for outpoint watches + * @blockheight: the block height where tx was found + * @txindex: position of tx in block (0 = coinbase) + * + * Called when bwatch detects a watched item in a block. + */ +typedef void (*watch_found_fn)(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex); + +typedef void (*watch_revert_fn)(struct lightningd *ld, + const char *suffix, + u32 blockheight); + +/** + * depth_found_fn - Handler for blockdepth watch notifications. + * @depth: new_height - confirm_height + 1 (always >= 1) + * @blockheight: current chain tip height + * + * Called once per new block. When the confirming block is reorged away, + * watch_revert_fn is called instead. + */ +typedef void (*depth_found_fn)(struct lightningd *ld, + const char *suffix, + u32 depth, + u32 blockheight); + +/** + * watchman_new - Create and initialize a new watchman instance + * @ctx: tal context to allocate from + * @ld: lightningd instance + * + * Returns a new watchman instance, loading pending operations from datastore. + */ +struct watchman *watchman_new(const tal_t *ctx, struct lightningd *ld); + +/** + * watchman_ack - Acknowledge a completed watch operation + * @ld: lightningd instance + * @op_id: the operation ID that was acknowledged + * + * Called when bwatch acknowledges a watch operation. + */ +void watchman_ack(struct lightningd *ld, const char *op_id); + +/** + * watchman_replay_pending - Replay all pending operations + * @ld: lightningd instance + * + * Resends all pending watch operations to bwatch. + * Call this when bwatch is ready (e.g., on startup). + */ +void watchman_replay_pending(struct lightningd *ld); + +#endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ From e24bd5620993ebe947c2b9e0983ca6557289fdb9 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:17:10 +0930 Subject: [PATCH 31/77] lightningd: add watchman storage and persistence skeleton Introduce the minimal storage scaffolding for the watchman module: - db_set_blobvar / db_get_blobvar helpers for persisting binary values (e.g. block hashes) in the SQL `vars` table. - load_tip(): recover last_processed_height and last_processed_hash from the wallet db. - apply_rescan(): honour --rescan by adjusting the loaded tip downward (negative = absolute height, positive = N blocks back). - watchman_new(): allocate the struct, initialise the pending-op array, and call load_tip + apply_rescan. Wire the watchman field into struct lightningd via a forward declaration; instantiation at startup lands in a later commit along with the rest of the wiring. --- db/exec.c | 33 +++++++++++++++++++ db/exec.h | 5 +++ lightningd/lightningd.h | 2 ++ lightningd/watchman.c | 70 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/db/exec.c b/db/exec.c index 382ad9f15e95..68c2ced55a3c 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 c852d9501e9a..242b2f52ff88 100644 --- a/db/exec.h +++ b/db/exec.h @@ -4,6 +4,7 @@ #include #include +#include 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 3b4e0e84d904..d2a4f23fc74b 100644 --- a/lightningd/lightningd.h +++ b/lightningd/lightningd.h @@ -12,6 +12,7 @@ #include struct amount_msat; +struct watchman; /* Various adjustable things. */ struct config { @@ -239,6 +240,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/watchman.c b/lightningd/watchman.c index 9a1ed50a8d50..60abf8123948 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -1,5 +1,71 @@ #include "config.h" +#include +#include +#include #include +#include -/* 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; +} From 1cabb924b4e3c66a9e2ecf0ca97a12abd98e4540 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:17:10 +0930 Subject: [PATCH 32/77] lightningd/plugin: add on_plugin_ready callback hook Add an optional callback on `struct plugins` that fires whenever a plugin transitions to INIT_COMPLETE (i.e. its `init` response has arrived). Invoked from plugin_config_cb just before notify_plugin_started. This lets subsystems react to plugin readiness without polluting the generic plugin lifecycle. The watchman module uses it to replay any pending watch operations to bwatch as soon as bwatch is up. --- lightningd/plugin.c | 2 ++ lightningd/plugin.h | 3 +++ 2 files changed, 5 insertions(+) diff --git a/lightningd/plugin.c b/lightningd/plugin.c index a9098ad0a81c..0005effbb22d 100644 --- a/lightningd/plugin.c +++ b/lightningd/plugin.c @@ -2095,6 +2095,8 @@ static void plugin_config_cb(const char *buffer, } if (tal_count(plugin->custom_msgs)) tell_connectd_custommsgs(plugin->plugins); + if (plugin->plugins->on_plugin_ready) + plugin->plugins->on_plugin_ready(plugin->plugins->ld, plugin); notify_plugin_started(plugin->plugins->ld, plugin); check_plugins_initted(plugin->plugins); } diff --git a/lightningd/plugin.h b/lightningd/plugin.h index 23b7554b3702..d3df785f086f 100644 --- a/lightningd/plugin.h +++ b/lightningd/plugin.h @@ -156,6 +156,9 @@ struct plugins { /* Whether to save all IO to a file */ char *dev_save_io; + + /* Optional callback invoked whenever a plugin reaches INIT_COMPLETE. */ + void (*on_plugin_ready)(struct lightningd *ld, struct plugin *plugin); }; /** From 743949f40e03dfc5bce975c458b14559d63596df Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:17:10 +0930 Subject: [PATCH 33/77] lightningd: add send_to_bwatch and watchman_ack Introduce the outbound RPC path from watchman to the bwatch plugin plus the ack lifecycle that drops a pending op once bwatch confirms it. - struct pending_op carries an op_id of the form "{method}:{owner}" (e.g. "addscriptpubkeywatch:wallet/p2wpkh/42"); method and owner are recoverable without a separate field. - Datastore helpers (make_key, db_save, db_remove) persist pending ops at ["watchman", "pending", op_id] for crash recovery. - send_to_bwatch finds the bwatch plugin via find_plugin_for_command on the method name; if bwatch is not yet INIT_COMPLETE, the send is silently dropped (the op stays queued and will be replayed when bwatch comes up). Otherwise it builds a JSON-RPC request with the owner suffix and the caller-supplied json_params body, registers bwatch_ack_response as the callback, and sends it. - watchman_ack searches pending_ops by op_id; on a hit it removes the datastore entry and drops the in-memory op. db_save and send_to_bwatch are marked __attribute__((unused)) here because their callers (enqueue_op, watchman_replay_pending) land in the next commit; the markers are removed there. --- lightningd/watchman.c | 184 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 176 insertions(+), 8 deletions(-) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 60abf8123948..fca904eb85f3 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -1,22 +1,73 @@ #include "config.h" +#include +#include +#include +#include +#include +#include #include +#include #include #include +#include #include #include /* - * 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. + * Watchman is the interface between lightningd and the bwatch plugin. + * It manages a pending operation queue to ensure reliable delivery of + * watch add/delete requests to bwatch, even across crashes. * - * 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. + * Architecture: + * - Subsystems (channel, onchaind, wallet) call watchman_add/watchman_del + * - Watchman queues operations and sends them to bwatch via RPC + * - Operations stay in queue until bwatch acknowledges them + * - On crash/restart, pending ops are replayed from datastore + * - Bwatch handles duplicate operations idempotently */ +/* A pending operation - method and params to send to bwatch */ +struct pending_op { + /* "{method}:{owner}", e.g. "addscriptpubkeywatch:wallet/p2wpkh/42". + * Method and owner are recoverable from this without a separate field. */ + const char *op_id; + const char *json_params; /* JSON params to send to bwatch */ +}; + + +/* + * Datastore persistence helpers + * Pending operations are stored at ["watchman", "pending", op_id] + */ + +/* Generate datastore key for a pending operation */ +static const char **make_key(const tal_t *ctx, const char *op_id TAKES) +{ + return mkdatastorekey(ctx, "watchman", "pending", op_id); +} + + +/* Persist a pending operation to the datastore for crash recovery. + * The method is encoded in op_id (see struct pending_op), so we store + * only json_params as the value. */ +__attribute__((unused)) +static void db_save(struct watchman *wm, const struct pending_op *op) +{ + const char **key = make_key(tmpctx, op->op_id); + const u8 *data = (const u8 *)op->json_params; + if (wallet_datastore_get(tmpctx, wm->ld->wallet, key, NULL)) + wallet_datastore_update(wm->ld->wallet, key, data); + else + wallet_datastore_create(wm->ld->wallet, key, data); +} + +/* Remove a pending operation from the datastore */ +static void db_remove(struct watchman *wm, const char *op_id) +{ + const char **key = make_key(tmpctx, op_id); + wallet_datastore_remove(wm->ld->wallet, key); +} + static void load_tip(struct watchman *wm) { struct db *db = wm->ld->wallet->db; @@ -69,3 +120,120 @@ struct watchman *watchman_new(const tal_t *ctx, struct lightningd *ld) return wm; } + +/* Per-request context for bwatch_ack_response. Carries the bare op_id so the + * callback never needs to parse the JSON-RPC response id. */ +struct bwatch_ack_arg { + struct watchman *wm; + const char *op_id; /* "{method}:{owner}", e.g. "addscriptpubkeywatch:wallet/p2wpkh/42" */ +}; + +/* Response callback for bwatch RPC requests; handles both success and error. */ +static void bwatch_ack_response(const char *buffer, + const jsmntok_t *toks, + const jsmntok_t *idtok UNUSED, + struct bwatch_ack_arg *arg) +{ + const jsmntok_t *err = json_get_member(buffer, toks, "error"); + + if (err) { + log_unusual(arg->wm->ld->log, "bwatch operation %s failed: %.*s", + arg->op_id, json_tok_full_len(err), json_tok_full(buffer, err)); + } else { + log_debug(arg->wm->ld->log, "Acknowledged pending op: %s", arg->op_id); + } + + watchman_ack(arg->wm->ld, arg->op_id); +} + +/* op_id is "{method}:{owner}"; return the owner suffix. */ +static const char *owner_from_op_id(const char *op_id) +{ + const char *colon = strchr(op_id, ':'); + return colon ? colon + 1 : ""; +} + +/* op_id is "{method}:{owner}"; return the method prefix. */ +__attribute__((unused)) +static const char *method_from_op_id(const tal_t *ctx, const char *op_id) +{ + const char *colon = strchr(op_id, ':'); + assert(colon); /* op_id must always be "{method}:{owner}" */ + return tal_strndup(ctx, op_id, colon - op_id); +} + +/* Send an RPC request to the bwatch plugin. + * op_id must be "{method}:{owner}", e.g. "addscriptpubkeywatch:wallet/p2wpkh/42". */ +__attribute__((unused)) +static void send_to_bwatch(struct watchman *wm, const char *method, + const char *op_id, const char *json_params) +{ + struct plugin *bwatch; + struct jsonrpc_request *req; + const char *owner; + size_t len; + + /* Find bwatch plugin by the command it registers */ + bwatch = find_plugin_for_command(wm->ld, method); + if (!bwatch) { + log_broken(wm->ld->log, "bwatch plugin not found, cannot send %s", method); + return; + } + + if (bwatch->plugin_state != INIT_COMPLETE) { + log_debug(wm->ld->log, "bwatch plugin not ready (state %d), queuing %s %s", + bwatch->plugin_state, method, op_id); + return; + } + + struct bwatch_ack_arg *arg = tal(tmpctx, struct bwatch_ack_arg); + arg->wm = wm; + arg->op_id = tal_strdup(arg, op_id); + + req = jsonrpc_request_start(wm, method, op_id, bwatch->log, + NULL, bwatch_ack_response, arg); + + /* Parent arg to req so it's freed when the request is freed, + * regardless of whether the callback fires. */ + tal_steal(req, arg); + + owner = owner_from_op_id(op_id); + if (!streq(owner, "")) + json_add_string(req->stream, "owner", owner); + + /* json_params is a JSON object string like {"type":"...","scriptpubkey":"...","start_block":N}. + * Append the rest (skip outer braces) so we get type, scriptpubkey, start_block, etc. */ + len = strlen(json_params); + if (len >= 2 && json_params[0] == '{' && json_params[len-1] == '}') { + json_stream_append(req->stream, ",", 1); + json_stream_append(req->stream, json_params + 1, len - 2); + } else { + json_stream_append(req->stream, ",", 1); + json_stream_append(req->stream, json_params, len); + } + + jsonrpc_request_end(req); + plugin_request_send(bwatch, req); +} + +/** + * watchman_ack - Acknowledge a completed watch operation + * + * Called when bwatch confirms it has processed an add/del operation. + * Removes the operation from the pending queue and datastore. + * op_id must be the bare stored id (e.g. "add:wallet/p2wpkh/0"), not the + * full JSON-RPC response id. + */ +void watchman_ack(struct lightningd *ld, const char *op_id) +{ + struct watchman *wm = ld->watchman; + + 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); + tal_free(wm->pending_ops[i]); + tal_arr_remove(&wm->pending_ops, i); + return; + } + } +} From a95caa115e7cf913d106dd48f77f4229728abe47 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:20:43 +0930 Subject: [PATCH 34/77] lightningd: replay pending ops on plugin ready Both bwatch and watchman must be crash-resistant: a watch_send or an add_watch/del_watch op may be in flight when lightningd crashes, and neither side is allowed to lose it. We solve this by persisting every pending op to the datastore in enqueue_op and dropping it from the datastore in watchman_ack. On startup load_pending_ops rebuilds the in-memory queue from the datastore, and watchman_on_plugin_ready replays it once bwatch reaches INIT_COMPLETE. watchman_add cancels any prior add for the same owner; watchman_del cancels any pending add for the same owner before queueing the delete. This keeps the queue from accumulating stale or self-cancelling op pairs across restarts. --- lightningd/watchman.c | 144 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 3 deletions(-) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index fca904eb85f3..169262de7999 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -50,7 +50,6 @@ static const char **make_key(const tal_t *ctx, const char *op_id TAKES) /* Persist a pending operation to the datastore for crash recovery. * The method is encoded in op_id (see struct pending_op), so we store * only json_params as the value. */ -__attribute__((unused)) static void db_save(struct watchman *wm, const struct pending_op *op) { const char **key = make_key(tmpctx, op->op_id); @@ -68,6 +67,16 @@ static void db_remove(struct watchman *wm, const char *op_id) wallet_datastore_remove(wm->ld->wallet, key); } +__attribute__((unused)) +static void save_tip(struct watchman *wm) +{ + struct db *db = wm->ld->wallet->db; + db_set_intvar(db, "last_watchman_block_height", wm->last_processed_height); + db_set_blobvar(db, "last_watchman_block_hash", + (const u8 *)&wm->last_processed_hash, + sizeof(wm->last_processed_hash)); +} + static void load_tip(struct watchman *wm) { struct db *db = wm->ld->wallet->db; @@ -82,6 +91,43 @@ static void load_tip(struct watchman *wm) } } +/* Load all pending operations from datastore on startup */ +static void load_pending_ops(struct watchman *wm) +{ + const char **startkey = mkdatastorekey(tmpctx, "watchman", "pending"); + const char **key; + const u8 *data; + u64 generation; + struct db_stmt *stmt; + + for (stmt = wallet_datastore_first(tmpctx, wm->ld->wallet, startkey, + &key, &data, &generation); + stmt; + stmt = wallet_datastore_next(tmpctx, startkey, stmt, + &key, &data, &generation)) { + if (tal_count(key) != 3) + continue; + + /* op_id is the datastore key; method is the prefix before ':'. + * Malformed keys (no ':') are skipped — they can't be replayed. */ + if (!strchr(key[2], ':')) { + log_broken(wm->ld->log, + "Skipping malformed pending op key '%s' (no ':' separator)", + key[2]); + continue; + } + + struct pending_op *op = tal(wm, struct pending_op); + op->op_id = tal_strdup(op, key[2]); + op->json_params = tal_strdup(op, (const char *)data); + tal_arr_expand(&wm->pending_ops, op); + + log_debug(wm->ld->log, "Loaded pending op: %s", op->op_id); + } +} + +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) @@ -112,12 +158,16 @@ struct watchman *watchman_new(const tal_t *ctx, struct lightningd *ld) wm->ld = ld; wm->pending_ops = tal_arr(wm, struct pending_op *, 0); + load_pending_ops(wm); 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)); + /* Replay pending ops exactly when bwatch transitions to INIT_COMPLETE. */ + ld->plugins->on_plugin_ready = watchman_on_plugin_ready; + return wm; } @@ -154,7 +204,6 @@ static const char *owner_from_op_id(const char *op_id) } /* op_id is "{method}:{owner}"; return the method prefix. */ -__attribute__((unused)) static const char *method_from_op_id(const tal_t *ctx, const char *op_id) { const char *colon = strchr(op_id, ':'); @@ -164,7 +213,6 @@ static const char *method_from_op_id(const tal_t *ctx, const char *op_id) /* Send an RPC request to the bwatch plugin. * op_id must be "{method}:{owner}", e.g. "addscriptpubkeywatch:wallet/p2wpkh/42". */ -__attribute__((unused)) static void send_to_bwatch(struct watchman *wm, const char *method, const char *op_id, const char *json_params) { @@ -216,6 +264,58 @@ static void send_to_bwatch(struct watchman *wm, const char *method, plugin_request_send(bwatch, req); } +/* Queue an operation, persist it for crash recovery, and send to bwatch. */ +static void enqueue_op(struct watchman *wm, const char *method, + const char *op_id, const char *json_params) +{ + struct pending_op *op = tal(wm, struct pending_op); + op->op_id = tal_strdup(op, op_id); + op->json_params = tal_strdup(op, json_params); + tal_arr_expand(&wm->pending_ops, op); + db_save(wm, op); + send_to_bwatch(wm, method, op_id, json_params); +} + +/* Internal: queue an add for a specific per-type bwatch command. */ +__attribute__((unused)) +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); + + /* Remove any existing add for this owner */ + watchman_ack(ld, op_id); + enqueue_op(wm, method, op_id, json_params); +} + +/** + * watchman_del - Queue a delete watch operation + * + * Simply queues the operation and sends to bwatch. + * Bwatch handles duplicate deletes idempotently. + * Cancels any pending add for this owner. + */ +__attribute__((unused)) +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); + + /* Cancel any pending add for this owner — the add method is different + * from the del method, so scan by owner rather than constructing the + * add op_id directly. */ + for (size_t i = 0; i < tal_count(wm->pending_ops); i++) { + if (strstarts(wm->pending_ops[i]->op_id, "add") && + streq(owner_from_op_id(wm->pending_ops[i]->op_id), owner)) { + watchman_ack(ld, wm->pending_ops[i]->op_id); + break; + } + } + enqueue_op(wm, method, op_id, json_params); +} + /** * watchman_ack - Acknowledge a completed watch operation * @@ -237,3 +337,41 @@ void watchman_ack(struct lightningd *ld, const char *op_id) } } } + +/** + * watchman_replay_pending - Resend all pending operations to bwatch + * + * Called on startup after bwatch is ready, to ensure any operations + * that were pending before a crash are sent to bwatch. + */ +void watchman_replay_pending(struct lightningd *ld) +{ + struct watchman *wm = ld->watchman; + + 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), + op->op_id, op->json_params); + } +} + +/* 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) +{ + struct watchman *wm = ld->watchman; + + if (!wm) + return; + /* Check if this is bwatch by seeing if it owns the "addscriptpubkeywatch" method. */ + if (find_plugin_for_command(ld, "addscriptpubkeywatch") != plugin) + return; + + if (wm->last_processed_height > 0) { + log_debug(ld->log, "bwatch reached INIT_COMPLETE, replaying pending ops (height=%u)", + wm->last_processed_height); + watchman_replay_pending(ld); + /* TODO: notify_block_added(ld, height, &hash) once that helper's + * signature is migrated in Group H (chaintopology removal). */ + } +} From 15e27f8466be6d1364a042ff14641cec6eb38062 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:22:10 +0930 Subject: [PATCH 35/77] lightningd: register getwatchmanheight + chaininfo bwatch RPCs Register the two startup RPCs that bwatch calls on launch: - getwatchmanheight: bwatch asks how far we've already processed the chain so it knows what height to (re)scan from. Returns {height, blockhash?} from wm->last_processed_{height,hash}. - chaininfo: bwatch reports the chain name, header/block counts, and IBD status. We fatal() on a network mismatch (wrong bitcoind), toggle bitcoind->synced based on IBD/header-vs-block lag, fire notify_new_block on the transition to synced, and remember the blockcount on watchman. --- lightningd/watchman.c | 99 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 169262de7999..96fa5f5d9b1f 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -1,11 +1,17 @@ #include "config.h" #include +#include #include #include +#include +#include +#include #include #include #include #include +#include +#include #include #include #include @@ -375,3 +381,96 @@ static void watchman_on_plugin_ready(struct lightningd *ld, struct plugin *plugi * signature is migrated in Group H (chaintopology removal). */ } } + +/** + * json_getwatchmanheight - RPC handler to return watchman's last processed height + * + * Called by bwatch on startup to determine what height to rescan from. + */ +static struct command_result *json_getwatchmanheight(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNUSED, + const jsmntok_t *params) +{ + struct watchman *wm = cmd->ld->watchman; + struct json_stream *response; + u32 height; + + if (!param(cmd, buffer, params, NULL)) + return command_param_failed(); + + height = wm ? wm->last_processed_height : 0; + log_debug(cmd->ld->log, "getwatchmanheight: returning height=%u (wm=%s)", + height, wm ? "ok" : "NULL"); + response = json_stream_success(cmd); + json_add_u32(response, "height", height); + if (wm && wm->last_processed_height > 0) + json_add_string(response, "blockhash", + fmt_bitcoin_blkid(response, &wm->last_processed_hash)); + return command_success(cmd, response); +} + +static const struct json_command getwatchmanheight_command = { + "getwatchmanheight", + json_getwatchmanheight, +}; +AUTODATA(json_command, &getwatchmanheight_command); + +/** + * json_chaininfo - RPC handler for chaininfo from bwatch + * + * Called by bwatch on startup to inform watchman about the chain name, + * IBD status, and sync state. Validates we're on the right network and + * sets bitcoind->synced accordingly. + */ +static struct command_result *json_chaininfo(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNUSED, + const jsmntok_t *params) +{ + const char *chain; + u32 *headercount, *blockcount; + bool *ibd; + + if (!param(cmd, buffer, params, + p_req("chain", param_string, &chain), + p_req("headercount", param_number, &headercount), + p_req("blockcount", param_number, &blockcount), + p_req("ibd", param_bool, &ibd), + NULL)) + return command_param_failed(); + + if (!streq(chain, chainparams->bip70_name)) + fatal("Wrong network! Our Bitcoin backend is running on '%s'," + " but we expect '%s'.", chain, chainparams->bip70_name); + if (*ibd) { + log_unusual(cmd->ld->log, + "Waiting for initial block download" + " (this can take a while!)"); + cmd->ld->topology->bitcoind->synced = false; + } else if (*headercount != *blockcount) { + log_unusual(cmd->ld->log, + "Waiting for bitcoind to catch up" + " (%u blocks of %u)", + *blockcount, *headercount); + cmd->ld->topology->bitcoind->synced = false; + } else { + if (!cmd->ld->topology->bitcoind->synced) + log_info(cmd->ld->log, "Bitcoin backend now synced"); + cmd->ld->topology->bitcoind->synced = true; + notify_new_block(cmd->ld); + } + + cmd->ld->watchman->bitcoind_blockcount = *blockcount; + + struct json_stream *response = json_stream_success(cmd); + json_add_string(response, "chain", chain); + json_add_bool(response, "synced", cmd->ld->topology->bitcoind->synced); + return command_success(cmd, response); +} + +static const struct json_command chaininfo_command = { + "chaininfo", + json_chaininfo, +}; +AUTODATA(json_command, &chaininfo_command); From 46a7c81a8c6505e7a6a22eb65bf9b88d6ae04fd8 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:25:46 +0930 Subject: [PATCH 36/77] lightningd: register block_processed + revert_block_processed RPCs bwatch's polling loop calls these two RPCs to keep watchman's chain tip in sync. - block_processed fires after bwatch finishes scanning a block. Watchman advances last_processed_{height,hash}, persists them, and fires notify_new_block. - revert_block_processed fires when bwatch detects a reorg. Watchman rewinds last_processed_{height,hash} to the supplied values and persists them. --- lightningd/watchman.c | 115 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 96fa5f5d9b1f..676aedbf3dfc 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -6,12 +6,13 @@ #include #include #include -#include +#include #include #include #include #include #include +#include #include #include #include @@ -73,7 +74,6 @@ static void db_remove(struct watchman *wm, const char *op_id) wallet_datastore_remove(wm->ld->wallet, key); } -__attribute__((unused)) static void save_tip(struct watchman *wm) { struct db *db = wm->ld->wallet->db; @@ -382,6 +382,117 @@ static void watchman_on_plugin_ready(struct lightningd *ld, struct plugin *plugi } } +static struct command_result *param_bitcoin_blkid_cmd(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct bitcoin_blkid **blkid) +{ + *blkid = tal(cmd, struct bitcoin_blkid); + if (!json_to_bitcoin_blkid(buffer, tok, *blkid)) + return command_fail_badparam(cmd, name, buffer, tok, + "Expected a blockhash"); + return NULL; +} + +static struct command_result *json_revert_block_processed(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNUSED, + const jsmntok_t *params) +{ + struct watchman *wm = cmd->ld->watchman; + u32 *blockheight; + struct bitcoin_blkid *blockhash; + + if (!param(cmd, buffer, params, + p_req("blockheight", param_number, &blockheight), + p_req("blockhash", param_bitcoin_blkid_cmd, &blockhash), + NULL)) + return command_param_failed(); + + if (!wm) + return command_fail(cmd, LIGHTNINGD, "Watchman not initialized"); + + log_debug(wm->ld->log, "block_reverted: %u -> %u", + wm->last_processed_height, *blockheight); + wm->last_processed_height = *blockheight; + wm->last_processed_hash = *blockhash; + save_tip(wm); + + struct json_stream *response = json_stream_success(cmd); + json_add_u32(response, "blockheight", *blockheight); + return command_success(cmd, response); +} + +static const struct json_command revert_block_processed_command = { + "revert_block_processed", + json_revert_block_processed, +}; +AUTODATA(json_command, &revert_block_processed_command); + +/** + * json_block_processed - RPC handler for block_processed notifications from bwatch + * + * Called by bwatch after it finishes processing all watches in a block. + * We track this height to know where bwatch is in the chain, which helps + * during startup/reorg scenarios. + */ +static struct command_result *json_block_processed(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNUSED, + const jsmntok_t *params) +{ + struct watchman *wm = cmd->ld->watchman; + u32 *blockheight; + struct bitcoin_blkid *blockhash; + + if (!param(cmd, buffer, params, + p_req("blockheight", param_number, &blockheight), + p_req("blockhash", param_bitcoin_blkid_cmd, &blockhash), + NULL)) + return command_param_failed(); + + if (!wm) + return command_fail(cmd, LIGHTNINGD, "Watchman not initialized"); + + if (*blockheight != wm->last_processed_height) { + log_info(wm->ld->log, "block_processed: %u -> %u", + wm->last_processed_height, *blockheight); + + /* Fresh node: replay wallet watches now that bwatch->current_height > 0, + * so add_watch_and_maybe_rescan will trigger historical rescans. */ + if (wm->last_processed_height == 0) { + log_debug(wm->ld->log, + "First block_processed on fresh node, replaying pending ops"); + watchman_replay_pending(wm->ld); + } + + wm->last_processed_height = *blockheight; + wm->last_processed_hash = *blockhash; + save_tip(wm); + /* TODO: notify_block_added(wm->ld, *blockheight, blockhash) once + * its signature is migrated in Group H (chaintopology removal). */ + send_account_balance_snapshot(wm->ld); + } + + /* TODO: channel_block_processed(wm->ld, *blockheight) lands in Group G + * when channel.c is migrated off chaintopology onto watchman. */ + notify_new_block(wm->ld); + + struct json_stream *response = json_stream_success(cmd); + json_add_u32(response, "blockheight", *blockheight); + if (wm->last_processed_height > 0) + json_add_string(response, "blockhash", + fmt_bitcoin_blkid(response, &wm->last_processed_hash)); + return command_success(cmd, response); +} + +static const struct json_command block_processed_command = { + "block_processed", + json_block_processed, +}; +AUTODATA(json_command, &block_processed_command); + /** * json_getwatchmanheight - RPC handler to return watchman's last processed height * From a7f30f8c3301dd706eace7d4e15ad0949ef6d2ab Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:25:57 +0930 Subject: [PATCH 37/77] lightningd: register watch_found / watch_revert dispatch Add depth_handlers[] / watch_handlers[] dispatch tables keyed by owner prefix (sentinel-only for now; entries land alongside their handlers in later commits) and the json_watch_found / json_watch_revert RPCs that bwatch calls into. --- lightningd/watchman.c | 185 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 676aedbf3dfc..b9344527515b 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -1,6 +1,7 @@ #include "config.h" #include #include +#include #include #include #include @@ -382,6 +383,94 @@ static void watchman_on_plugin_ready(struct lightningd *ld, struct plugin *plugi } } +/* Dispatch table - add new watch types here */ +static const struct depth_dispatch { + const char *prefix; + depth_found_fn handler; + watch_revert_fn revert; +} depth_handlers[] = { + /* Entries added in subsequent commits alongside their handler functions. */ + { NULL, NULL, NULL }, +}; + +static const struct watch_dispatch { + const char *prefix; + watch_found_fn handler; + watch_revert_fn revert; +} watch_handlers[] = { + /* Entries added in subsequent commits alongside their handler functions. */ + { NULL, NULL, NULL }, +}; + +/* dispatch_watch_found: search depth_handlers then watch_handlers for owner. + * depth is NULL for tx-based notifications, set for blockdepth notifications. */ +static void dispatch_watch_found(struct lightningd *ld, + const char *owner, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex, + const u32 *depth) +{ + for (size_t i = 0; i < ARRAY_SIZE(depth_handlers); i++) { + if (!depth_handlers[i].prefix) + continue; + if (strstarts(owner, depth_handlers[i].prefix)) { + const char *suffix = owner + strlen(depth_handlers[i].prefix); + depth_handlers[i].handler(ld, suffix, *depth, blockheight); + return; + } + } + for (size_t i = 0; i < ARRAY_SIZE(watch_handlers); i++) { + if (!watch_handlers[i].prefix) + continue; + if (strstarts(owner, watch_handlers[i].prefix)) { + const char *suffix = owner + strlen(watch_handlers[i].prefix); + watch_handlers[i].handler(ld, suffix, tx, outnum, blockheight, txindex); + return; + } + } + log_debug(ld->log, "No handler for watch owner: %s", owner); +} + +static void dispatch_watch_revert(struct lightningd *ld, + const char *owner, + u32 blockheight) +{ + for (size_t i = 0; i < ARRAY_SIZE(depth_handlers); i++) { + if (!depth_handlers[i].prefix) + continue; + if (strstarts(owner, depth_handlers[i].prefix)) { + const char *suffix = owner + strlen(depth_handlers[i].prefix); + depth_handlers[i].revert(ld, suffix, blockheight); + return; + } + } + for (size_t i = 0; i < ARRAY_SIZE(watch_handlers); i++) { + if (!watch_handlers[i].prefix) + continue; + if (strstarts(owner, watch_handlers[i].prefix)) { + const char *suffix = owner + strlen(watch_handlers[i].prefix); + watch_handlers[i].revert(ld, suffix, blockheight); + return; + } + } + log_debug(ld->log, "No revert handler for watch owner: %s", owner); +} + +static struct command_result *param_bitcoin_tx(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct bitcoin_tx **tx) +{ + *tx = bitcoin_tx_from_hex(cmd, buffer + tok->start, tok->end - tok->start); + if (!*tx) + return command_fail_badparam(cmd, name, buffer, tok, + "Expected a hex-encoded transaction"); + return NULL; +} + static struct command_result *param_bitcoin_blkid_cmd(struct command *cmd, const char *name, const char *buffer, @@ -395,6 +484,102 @@ static struct command_result *param_bitcoin_blkid_cmd(struct command *cmd, return NULL; } +/** + * json_watch_found - RPC handler for watch_found notifications from bwatch + * + * Handles both tx-based watches (scriptpubkey, outpoint, txid, scid) and + * blockdepth watches. Dispatches by owner prefix. + * + * For WATCH_SCID, bwatch may omit "tx" and "txindex" to signal that the + * SCID's expected tx/output was absent from the encoded block ("not found"). + * The handler (gossip_scid_watch_found) detects this via tx==NULL. + */ +static struct command_result *json_watch_found(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNUSED, + const jsmntok_t *params) +{ + struct watchman *wm = cmd->ld->watchman; + const char **owners; + u32 *blockheight, *txindex, *index, *depth; + struct bitcoin_tx *tx; + + if (!param_check(cmd, buffer, params, + p_req("blockheight", param_number, &blockheight), + p_req("owners", param_string_array, &owners), + p_opt("tx", param_bitcoin_tx, &tx), + p_opt("txindex", param_number, &txindex), + p_opt("index", param_number, &index), + p_opt("depth", param_number, &depth), + NULL)) + return command_param_failed(); + + /* For normal tx-based watches tx+txindex are required. + * Exception: WATCH_SCID owners send watch_found with tx==NULL to + * signal "not found"; their handler checks for this explicitly. */ + if (!depth && !tx && txindex) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "txindex provided without tx in watch_found"); + if (!depth && tx && !txindex) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "tx provided without txindex in watch_found"); + + assert(wm); + if (command_check_only(cmd)) + return command_check_done(cmd); + + log_debug(cmd->ld->log, "watch_found at block %u%s", *blockheight, + depth ? " (blockdepth)" : ""); + for (size_t i = 0; i < tal_count(owners); i++) + dispatch_watch_found(cmd->ld, owners[i], tx, + index ? *index : 0, + *blockheight, + txindex ? *txindex : 0, + depth); + + struct json_stream *response = json_stream_success(cmd); + json_add_u32(response, "blockheight", *blockheight); + return command_success(cmd, response); +} + +static const struct json_command watch_found_command = { + "watch_found", + json_watch_found, +}; +AUTODATA(json_command, &watch_found_command); + +/** + * json_watch_revert - RPC handler for watch_revert notifications from bwatch + * + * Called when a watched item's confirming block is reorged away. Dispatches + * to the appropriate revert handler (depth or tx) based on owner prefix. + */ +static struct command_result *json_watch_revert(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNUSED, + const jsmntok_t *params) +{ + const char *owner; + u32 *blockheight; + + if (!param(cmd, buffer, params, + p_req("owner", param_string, &owner), + p_req("blockheight", param_number, &blockheight), + NULL)) + return command_param_failed(); + + dispatch_watch_revert(cmd->ld, owner, *blockheight); + struct json_stream *response = json_stream_success(cmd); + json_add_u32(response, "blockheight", *blockheight); + return command_success(cmd, response); +} + +static const struct json_command watch_revert_command = { + "watch_revert", + json_watch_revert, +}; +AUTODATA(json_command, &watch_revert_command); + static struct command_result *json_revert_block_processed(struct command *cmd, const char *buffer, const jsmntok_t *obj UNUSED, From d147b21d390da4484a6c311bea9d2bd87db73304 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:28:15 +0930 Subject: [PATCH 38/77] lightningd: add typed watchman_watch_scriptpubkey + unwatch Public wrappers around the internal watchman_add/watchman_del helpers that callers (wallet, channel) will use to register/remove WATCH_SCRIPTPUBKEY entries. Drops the unused-attr from watchman_add and watchman_del now that they have real callers. --- lightningd/watchman.c | 25 +++++++++++++++++++++++-- lightningd/watchman.h | 13 +++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index b9344527515b..70a59416b9a2 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -284,7 +285,6 @@ static void enqueue_op(struct watchman *wm, const char *method, } /* Internal: queue an add for a specific per-type bwatch command. */ -__attribute__((unused)) static void watchman_add(struct lightningd *ld, const char *method, const char *owner, const char *json_params) { @@ -303,7 +303,6 @@ static void watchman_add(struct lightningd *ld, const char *method, * Bwatch handles duplicate deletes idempotently. * Cancels any pending add for this owner. */ -__attribute__((unused)) static void watchman_del(struct lightningd *ld, const char *method, const char *owner, const char *json_params) { @@ -383,6 +382,28 @@ static void watchman_on_plugin_ready(struct lightningd *ld, struct plugin *plugi } } +void watchman_watch_scriptpubkey(struct lightningd *ld, + const char *owner, + const u8 *scriptpubkey, + size_t script_len, + u32 start_block) +{ + watchman_add(ld, "addscriptpubkeywatch", owner, + tal_fmt(tmpctx, "{\"scriptpubkey\":\"%s\",\"start_block\":%u}", + tal_hexstr(tmpctx, scriptpubkey, script_len), + start_block)); +} + +void watchman_unwatch_scriptpubkey(struct lightningd *ld, + const char *owner, + const u8 *scriptpubkey, + size_t script_len) +{ + watchman_del(ld, "delscriptpubkeywatch", owner, + tal_fmt(tmpctx, "{\"scriptpubkey\":\"%s\"}", + tal_hexstr(tmpctx, scriptpubkey, script_len))); +} + /* Dispatch table - add new watch types here */ static const struct depth_dispatch { const char *prefix; diff --git a/lightningd/watchman.h b/lightningd/watchman.h index c06671a7b45a..a8ac88eaf40b 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -84,4 +84,17 @@ void watchman_ack(struct lightningd *ld, const char *op_id); */ void watchman_replay_pending(struct lightningd *ld); +/** Register a WATCH_SCRIPTPUBKEY — fires when @scriptpubkey appears in a tx output. */ +void watchman_watch_scriptpubkey(struct lightningd *ld, + const char *owner, + const u8 *scriptpubkey, + size_t script_len, + u32 start_block); + +/** Remove a WATCH_SCRIPTPUBKEY. */ +void watchman_unwatch_scriptpubkey(struct lightningd *ld, + const char *owner, + const u8 *scriptpubkey, + size_t script_len); + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ From 530632f52ae2ea8f353c578fbcb48fe297c2656b Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:28:46 +0930 Subject: [PATCH 39/77] lightningd: add typed watchman_watch_outpoint + unwatch Public wrappers around watchman_add/watchman_del for WATCH_OUTPOINT entries. Used by channel/onchaind/wallet to watch funding outpoints, HTLC outputs and UTXOs for spends. --- lightningd/watchman.c | 21 +++++++++++++++++++++ lightningd/watchman.h | 11 +++++++++++ 2 files changed, 32 insertions(+) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 70a59416b9a2..de2efe1e740a 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -404,6 +404,27 @@ void watchman_unwatch_scriptpubkey(struct lightningd *ld, tal_hexstr(tmpctx, scriptpubkey, script_len))); } +void watchman_watch_outpoint(struct lightningd *ld, + const char *owner, + const struct bitcoin_outpoint *outpoint, + u32 start_block) +{ + watchman_add(ld, "addoutpointwatch", owner, + tal_fmt(tmpctx, "{\"outpoint\":\"%s:%u\",\"start_block\":%u}", + fmt_bitcoin_txid(tmpctx, &outpoint->txid), + outpoint->n, start_block)); +} + +void watchman_unwatch_outpoint(struct lightningd *ld, + const char *owner, + const struct bitcoin_outpoint *outpoint) +{ + watchman_del(ld, "deloutpointwatch", owner, + tal_fmt(tmpctx, "{\"outpoint\":\"%s:%u\"}", + fmt_bitcoin_txid(tmpctx, &outpoint->txid), + outpoint->n)); +} + /* Dispatch table - add new watch types here */ static const struct depth_dispatch { const char *prefix; diff --git a/lightningd/watchman.h b/lightningd/watchman.h index a8ac88eaf40b..2f84c0043d1b 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -97,4 +97,15 @@ void watchman_unwatch_scriptpubkey(struct lightningd *ld, const u8 *scriptpubkey, size_t script_len); +/** Register a WATCH_OUTPOINT — fires when @outpoint is spent. */ +void watchman_watch_outpoint(struct lightningd *ld, + const char *owner, + const struct bitcoin_outpoint *outpoint, + u32 start_block); + +/** Remove a WATCH_OUTPOINT (e.g. during splice before re-adding for new outpoint). */ +void watchman_unwatch_outpoint(struct lightningd *ld, + const char *owner, + const struct bitcoin_outpoint *outpoint); + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ From 041073a3bbd2b01896c0546b09eccdd3e8321e82 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:29:07 +0930 Subject: [PATCH 40/77] lightningd: add typed watchman_watch_scid + unwatch Public wrappers around watchman_add/watchman_del for WATCH_SCID entries. Used by gossipd to confirm announced channels by asking bwatch to fetch the output at a given short_channel_id position. --- lightningd/watchman.c | 20 ++++++++++++++++++++ lightningd/watchman.h | 12 ++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index de2efe1e740a..cc0e1a578c5f 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -1,6 +1,7 @@ #include "config.h" #include #include +#include #include #include #include @@ -425,6 +426,25 @@ void watchman_unwatch_outpoint(struct lightningd *ld, outpoint->n)); } +void watchman_watch_scid(struct lightningd *ld, + const char *owner, + const struct short_channel_id *scid, + u32 start_block) +{ + watchman_add(ld, "addscidwatch", owner, + tal_fmt(tmpctx, "{\"scid\":\"%s\",\"start_block\":%u}", + fmt_short_channel_id(tmpctx, *scid), start_block)); +} + +void watchman_unwatch_scid(struct lightningd *ld, + const char *owner, + const struct short_channel_id *scid) +{ + watchman_del(ld, "delscidwatch", owner, + tal_fmt(tmpctx, "{\"scid\":\"%s\"}", + fmt_short_channel_id(tmpctx, *scid))); +} + /* Dispatch table - add new watch types here */ static const struct depth_dispatch { const char *prefix; diff --git a/lightningd/watchman.h b/lightningd/watchman.h index 2f84c0043d1b..147d8fb77fe6 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -7,6 +7,7 @@ struct lightningd; struct pending_op; +struct short_channel_id; /* lightningd's view of bwatch. bwatch lives in a separate process and tells * us about new/reverted blocks and watch hits via JSON-RPC; watchman tracks @@ -108,4 +109,15 @@ void watchman_unwatch_outpoint(struct lightningd *ld, const char *owner, const struct bitcoin_outpoint *outpoint); +/** Register a WATCH_SCID — fires when bwatch finds the output (for gossip get_txout). */ +void watchman_watch_scid(struct lightningd *ld, + const char *owner, + const struct short_channel_id *scid, + u32 start_block); + +/** Remove a WATCH_SCID. */ +void watchman_unwatch_scid(struct lightningd *ld, + const char *owner, + const struct short_channel_id *scid); + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ From aeb8864a2f0e9902004a3830ce9ae12101acb5d3 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 15:29:17 +0930 Subject: [PATCH 41/77] lightningd: add typed watchman_watch_blockdepth + unwatch Public wrappers around watchman_add/watchman_del for WATCH_BLOCKDEPTH entries. These fire once per new block while a tx accumulates confirmations, used by channeld for funding-depth tracking and by onchaind to drive CSV/HTLC maturity timers. --- lightningd/watchman.c | 16 ++++++++++++++++ lightningd/watchman.h | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index cc0e1a578c5f..87ad2635c541 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -445,6 +445,22 @@ void watchman_unwatch_scid(struct lightningd *ld, fmt_short_channel_id(tmpctx, *scid))); } +void watchman_watch_blockdepth(struct lightningd *ld, + const char *owner, + u32 confirm_height) +{ + watchman_add(ld, "addblockdepthwatch", owner, + tal_fmt(tmpctx, "{\"start_block\":%u}", confirm_height)); +} + +void watchman_unwatch_blockdepth(struct lightningd *ld, + const char *owner, + u32 confirm_height) +{ + watchman_del(ld, "delblockdepthwatch", owner, + tal_fmt(tmpctx, "{\"start_block\":%u}", confirm_height)); +} + /* Dispatch table - add new watch types here */ static const struct depth_dispatch { const char *prefix; diff --git a/lightningd/watchman.h b/lightningd/watchman.h index 147d8fb77fe6..d5221f2e89be 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -120,4 +120,19 @@ void watchman_unwatch_scid(struct lightningd *ld, const char *owner, const struct short_channel_id *scid); +/** + * watchman_watch_blockdepth - Register a WATCH_BLOCKDEPTH + * @ld: lightningd instance + * @owner: the owner identifier (e.g. "channel/funding_depth/42") + * @confirm_height: the block height where the tx of interest was confirmed + */ +void watchman_watch_blockdepth(struct lightningd *ld, + const char *owner, + u32 confirm_height); + +/** Remove a WATCH_BLOCKDEPTH. */ +void watchman_unwatch_blockdepth(struct lightningd *ld, + const char *owner, + u32 confirm_height); + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ From b1ed8deb149256cfa330ee5d9db6e6cdea418b6c Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 07:38:06 +0930 Subject: [PATCH 42/77] wallet: add our_outputs + our_txs schema migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The next commits move wallet UTXO and tx tracking off chaintopology and onto bwatch. bwatch doesn't maintain a blocks table, but the legacy utxoset, transactions and channeltxs tables all have FOREIGN KEY references into blocks(height) (CASCADE / SET NULL), so we can't just retarget the existing tables. Instead, introduce parallel tables (our_outputs, our_txs) without the blocks(height) FK. The new bwatch-driven code writes only to these, the legacy tables stay populated by the existing code path during this release so downgrade still works, and a future release can drop them once we're past the downgrade window. Schema only here — wallet handlers that write into these tables and the backfill from utxoset/transactions land in subsequent commits. --- wallet/migrations.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/wallet/migrations.c b/wallet/migrations.c index 83c61c0e9129..d76a298c7ac3 100644 --- a/wallet/migrations.c +++ b/wallet/migrations.c @@ -1084,6 +1084,35 @@ static const struct db_migration dbmigrations[] = { NULL, NULL}, {SQL("ALTER TABLE offers ADD COLUMN force_paths INTEGER DEFAULT 0;"), NULL, SQL("ALTER TABLE offers DROP COLUMN force_paths"), NULL}, + + /* v26.04: parallel wallet tables without the blocks(height) FK that + * utxoset/transactions carry, so bwatch-driven writes don't need a + * blocks table. Legacy tables stay for one release to keep downgrade + * working. */ + {SQL("CREATE TABLE our_outputs (" + " txid BLOB NOT NULL," + " outnum INTEGER NOT NULL," + " blockheight INTEGER NOT NULL," + " txindex INTEGER," + " scriptpubkey BLOB NOT NULL," + " satoshis BIGINT NOT NULL," + " spendheight INTEGER," + " keyindex INTEGER," + " reserved_til INTEGER," + " channel_dbid BIGINT," + " peer_id BLOB," + " commitment_point BLOB," + " csv INTEGER," + " PRIMARY KEY (txid, outnum)" + ")"), NULL, + SQL("DROP TABLE our_outputs"), NULL}, + {SQL("CREATE TABLE our_txs (" + " txid BLOB NOT NULL PRIMARY KEY," + " blockheight INTEGER NOT NULL," + " txindex INTEGER," + " rawtx BLOB" + ")"), NULL, + SQL("DROP TABLE our_txs"), NULL}, }; const struct db_migration *get_db_migrations(size_t *num) From 814725645f27a4a224f692243f6b3ef90158d478 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 08:45:58 +0930 Subject: [PATCH 43/77] wallet: add bwatch wallet UTXO infrastructure Lands the helpers used by the upcoming bwatch-driven scriptpubkey watch handlers: bwatch_got_utxo, wallet_watch_scriptpubkey_common, the our_outputs/our_txs writers, their undo helpers, and the shared revert handler used by the p2wpkh/p2tr/p2sh_p2wpkh dispatch entries. Also adds the owner_wallet_utxo() owner-string constructor. All static helpers are __attribute__((unused)) until the typed handler commits wire them into watchman's dispatch table. Public functions (wallet_add_our_output, wallet_add_our_tx, wallet_del_txout_annotation, wallet_del_tx_if_unreferenced, wallet_scriptpubkey_watch_revert) are dead code for the same reason. Coexists with the legacy got_utxo() / wallet_transaction_add() that write to utxoset/transactions; the bwatch path uses renamed variants (bwatch_got_utxo, wallet_add_our_tx) so both tables stay populated for the downgrade window. --- lightningd/watchman.c | 1 - lightningd/watchman.h | 16 ++ wallet/wallet.c | 329 ++++++++++++++++++++++++++++++++++++++++++ wallet/wallet.h | 40 +++++ 4 files changed, 385 insertions(+), 1 deletion(-) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 87ad2635c541..06cdf9dcea4b 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include diff --git a/lightningd/watchman.h b/lightningd/watchman.h index d5221f2e89be..445797e6fb2c 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -3,7 +3,9 @@ #include "config.h" #include +#include #include +#include struct lightningd; struct pending_op; @@ -135,4 +137,18 @@ void watchman_unwatch_blockdepth(struct lightningd *ld, const char *owner, u32 confirm_height); +/* + * Owner string constructors. + * + * Always use these instead of raw tal_fmt() to build owner strings. Sharing + * one constructor between watchman_watch_* and watchman_unwatch_* guarantees + * the strings are identical and the unwatch can never silently fail due to a + * format mismatch (e.g. %u vs PRIu64). + */ + +/* wallet/ owners */ +static inline const char *owner_wallet_utxo(const tal_t *ctx, + const struct bitcoin_outpoint *op) +{ return tal_fmt(ctx, "wallet/utxo/%s", fmt_bitcoin_outpoint(ctx, op)); } + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ diff --git a/wallet/wallet.c b/wallet/wallet.c index 91eb5079ff6b..01fc92b91202 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -16,14 +16,17 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include +#include #include #include #include @@ -7897,3 +7900,329 @@ void migrate_remove_chain_moves_duplicates(struct lightningd *ld, struct db *db) db_exec_prepared_v2(take(stmt)); } } + +/* ==================================================================== + * bwatch-driven wallet recording. + * + * When bwatch reports that a wallet-owned scriptpubkey appeared in a block + * (or that a previously-seen output was reorged away), the dispatch table + * in lightningd/watchman calls into the helpers below. They write to the + * `our_outputs` and `our_txs` tables, which are independent of the + * `utxoset` / `transactions` tables populated by the legacy chaintopology + * path. Both sets of tables coexist for one release so a node can + * downgrade cleanly. + * + * Some statics are marked __attribute__((unused)) because the dispatch + * entries that wire them in are added in follow-on patches in this series. + * ==================================================================== */ + +/* Map an addrtype to the string used in bwatch owner identifiers, e.g. + * ADDR_BECH32 -> "p2wpkh" giving "wallet/p2wpkh/". */ +static const char *wallet_addrtype_to_owner_prefix(enum addrtype addrtype) + __attribute__((unused)); +static const char *wallet_addrtype_to_owner_prefix(enum addrtype addrtype) +{ + switch (addrtype) { + case ADDR_BECH32: + return "p2wpkh"; + case ADDR_P2TR: + return "p2tr"; + case ADDR_P2SH_SEGWIT: + return "p2sh_p2wpkh"; + case ADDR_ALL: + break; + } + return NULL; +} + +/* Insert a wallet-owned UTXO row into our_outputs. If the outpoint was + * previously inserted unconfirmed (blockheight=0) and we now have a real + * blockheight, promote the row so coin selection can treat it as + * spendable. */ +void wallet_add_our_output(struct wallet *w, + const struct bitcoin_outpoint *outpoint, + u32 blockheight, u32 txindex, + const u8 *script, size_t script_len, + struct amount_sat sat, + u32 keyindex) +{ + struct db_stmt *stmt; + + stmt = db_prepare_v2(w->db, + SQL("INSERT OR IGNORE INTO our_outputs " + "(txid, outnum, blockheight, txindex, scriptpubkey, satoshis, keyindex) " + "VALUES (?, ?, ?, ?, ?, ?, ?);")); + db_bind_txid(stmt, &outpoint->txid); + db_bind_int(stmt, outpoint->n); + db_bind_int(stmt, blockheight); + db_bind_int(stmt, txindex); + db_bind_blob(stmt, script, script_len); + db_bind_amount_sat(stmt, sat); + db_bind_int(stmt, keyindex); + db_exec_prepared_v2(take(stmt)); + + if (blockheight != 0) { + stmt = db_prepare_v2(w->db, + SQL("UPDATE our_outputs SET blockheight = ?, txindex = ? " + "WHERE txid = ? AND outnum = ? AND blockheight < ?;")); + db_bind_int(stmt, blockheight); + db_bind_int(stmt, txindex); + db_bind_txid(stmt, &outpoint->txid); + db_bind_int(stmt, outpoint->n); + db_bind_int(stmt, blockheight); + db_exec_prepared_v2(take(stmt)); + } +} + +/* Insert (or replace) a wallet-relevant transaction in our_txs. */ +void wallet_add_our_tx(struct wallet *w, const struct wally_tx *tx, + u32 blockheight, u32 txindex) +{ + struct db_stmt *stmt; + struct bitcoin_txid txid; + + wally_txid(tx, &txid); + + stmt = db_prepare_v2(w->db, + SQL("INSERT OR REPLACE INTO our_txs " + "(txid, blockheight, txindex, rawtx) VALUES (?, ?, ?, ?);")); + db_bind_txid(stmt, &txid); + db_bind_int(stmt, blockheight); + db_bind_int(stmt, txindex); + db_bind_talarr(stmt, linearize_wtx(tmpctx, tx)); + db_exec_prepared_v2(take(stmt)); +} + +/* Record a freshly-discovered wallet output: insert it into our_outputs, + * arm bwatch to notify us when it's spent, and (if confirmed) emit a + * deposit coin movement. Bwatch counterpart to the legacy got_utxo() + * defined earlier in this file + * and the legacy one is removed (and this one renamed) once the + * chaintopology code path goes away. */ +static void bwatch_got_utxo(struct wallet *w, + u64 keyindex, + enum addrtype addrtype, + const struct wally_tx *wtx, + size_t outnum, + bool is_coinbase, + const u32 *blockheight, + u32 txindex, + struct bitcoin_outpoint *outpoint) + __attribute__((unused)); +static void bwatch_got_utxo(struct wallet *w, + u64 keyindex, + enum addrtype addrtype, + const struct wally_tx *wtx, + size_t outnum, + bool is_coinbase, + const u32 *blockheight, + u32 txindex, + struct bitcoin_outpoint *outpoint) +{ + struct utxo *utxo = tal(tmpctx, struct utxo); + const struct wally_tx_output *txout = &wtx->outputs[outnum]; + struct amount_asset asset = wally_tx_output_get_amount(txout); + + utxo->keyindex = keyindex; + /* This switch() pattern catches anyone adding new cases, plus + * runtime errors */ + switch (addrtype) { + case ADDR_P2SH_SEGWIT: + utxo->utxotype = UTXO_P2SH_P2WPKH; + goto type_ok; + case ADDR_BECH32: + utxo->utxotype = UTXO_P2WPKH; + goto type_ok; + case ADDR_P2TR: + utxo->utxotype = UTXO_P2TR; + goto type_ok; + case ADDR_ALL: + break; + } + abort(); + +type_ok: + utxo->amount = amount_asset_to_sat(&asset); + utxo->status = OUTPUT_STATE_AVAILABLE; + wally_txid(wtx, &utxo->outpoint.txid); + utxo->outpoint.n = outnum; + utxo->close_info = NULL; + utxo->is_in_coinbase = is_coinbase; + + utxo->blockheight = blockheight; + utxo->spendheight = NULL; + utxo->scriptPubkey = tal_dup_arr(utxo, u8, txout->script, txout->script_len, 0); + log_debug(w->log, "Owning output %zu %s (%s) txid %s%s%s", + outnum, + fmt_amount_sat(tmpctx, utxo->amount), + utxotype_to_str(utxo->utxotype), + fmt_bitcoin_txid(tmpctx, &utxo->outpoint.txid), + blockheight ? " CONFIRMED" : "", + is_coinbase ? " COINBASE" : ""); + + /* We only record final ledger movements */ + if (blockheight) { + struct chain_coin_mvt *mvt; + + mvt = new_coin_wallet_deposit(tmpctx, &utxo->outpoint, + *blockheight, + utxo->amount, + mk_mvt_tags(MVT_DEPOSIT)); + wallet_save_chain_mvt(w->ld, mvt); + } + + /* Persist the output and arm bwatch to fire on its spend; the owner + * string ("wallet/utxo/") is what bwatch echoes back to us + * in the spend notification. */ + wallet_add_our_output(w, &utxo->outpoint, + blockheight ? *blockheight : 0, + txindex, + utxo->scriptPubkey, tal_bytelen(utxo->scriptPubkey), + utxo->amount, + utxo->keyindex); + watchman_watch_outpoint(w->ld, + owner_wallet_utxo(tmpctx, &utxo->outpoint), + &utxo->outpoint, + (blockheight && *blockheight > 0) + ? *blockheight + : get_block_height(w->ld->topology)); + + wallet_annotate_txout(w, &utxo->outpoint, TX_WALLET_DEPOSIT, 0); + if (outpoint) + *outpoint = utxo->outpoint; +} + +/* Shared body of the per-addrtype wallet scriptpubkey dispatch handlers + * (one each for p2wpkh, p2tr, p2sh_p2wpkh). Cross-checks the matched + * output against any pending invoice, records the transaction, and + * stores the new UTXO. */ +static void wallet_watch_scriptpubkey_common(struct lightningd *ld, + u32 keyindex, + enum addrtype addrtype, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex) + __attribute__((unused)); +static void wallet_watch_scriptpubkey_common(struct lightningd *ld, + u32 keyindex, + enum addrtype addrtype, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex) +{ + struct wallet *w = ld->wallet; + const struct wally_tx_output *txout; + struct amount_asset asset; + bool is_coinbase = (txindex == 0); + struct amount_sat amount; + struct bitcoin_outpoint outpoint; + struct bitcoin_txid txid; + + if (outnum >= tx->wtx->num_outputs) { + log_broken(w->log, "Invalid outnum %zu for tx with %zu outputs", + outnum, tx->wtx->num_outputs); + return; + } + + txout = &tx->wtx->outputs[outnum]; + asset = wally_tx_output_get_amount(txout); + + /* L-BTC has multi-asset outputs; we only care about the L-BTC main + * asset here. */ + if (!amount_asset_is_main(&asset)) { + log_debug(w->log, "Ignoring non-main asset output"); + return; + } + + bitcoin_txid(tx, &txid); + outpoint.txid = txid; + outpoint.n = outnum; + amount = bitcoin_tx_output_get_amount_sat(tx, outnum); + + invoice_check_onchain_payment(ld, txout->script, amount, &outpoint); + + wallet_add_our_tx(w, tx->wtx, blockheight, txindex); + + bwatch_got_utxo(w, keyindex, addrtype, tx->wtx, outnum, is_coinbase, + &blockheight, txindex, &outpoint); + + log_debug(w->log, "Wallet watch found: keyindex=%u, addrtype=%d, amount=%s, blockheight=%u%s", + keyindex, addrtype, fmt_amount_sat(tmpctx, amount), blockheight, + is_coinbase ? " COINBASE" : ""); +} + +/* Undo wallet_annotate_txout for an output annotation. */ +void wallet_del_txout_annotation(struct wallet *w, + const struct bitcoin_outpoint *outpoint) +{ + struct db_stmt *stmt = db_prepare_v2(w->db, + SQL("DELETE FROM transaction_annotations " + "WHERE txid = ? AND idx = ? AND location = ?")); + db_bind_txid(stmt, &outpoint->txid); + db_bind_int(stmt, outpoint->n); + db_bind_int(stmt, OUTPUT_ANNOTATION); + db_exec_prepared_v2(take(stmt)); +} + +/* Undo wallet_add_our_output for a single outpoint. */ +static void undo_wallet_add_our_output(struct wallet *w, + const struct bitcoin_outpoint *outpoint) +{ + struct db_stmt *stmt = db_prepare_v2(w->db, + SQL("DELETE FROM our_outputs WHERE txid = ? AND outnum = ?")); + db_bind_txid(stmt, &outpoint->txid); + db_bind_int(stmt, outpoint->n); + db_exec_prepared_v2(take(stmt)); +} + +/* Undo wallet_add_our_tx: removes from our_txs only if no our_outputs row + * still references it. */ +void wallet_del_tx_if_unreferenced(struct wallet *w, + const struct bitcoin_txid *txid) +{ + struct db_stmt *stmt = db_prepare_v2(w->db, + SQL("DELETE FROM our_txs WHERE txid = ?" + " AND NOT EXISTS (SELECT 1 FROM our_outputs WHERE txid = ?)")); + db_bind_txid(stmt, txid); + db_bind_txid(stmt, txid); + db_exec_prepared_v2(take(stmt)); +} + +/* Reorg-time counterpart to wallet_watch_scriptpubkey_common: drops every + * our_outputs row for this keyindex confirmed at @blockheight, removes + * the matching outpoint watch from bwatch, the txout annotation, and the + * cached transaction (if no other output still references it). The + * deposit chain movement and any invoice payment state recorded at the + * time are intentionally left in place — those side effects can't be + * meaningfully undone. */ +void wallet_scriptpubkey_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight) +{ + struct wallet *w = ld->wallet; + struct db_stmt *stmt; + struct bitcoin_outpoint outpoint; + u64 keyindex = strtoull(suffix, NULL, 10); + + stmt = db_prepare_v2(w->db, + SQL("SELECT txid, outnum FROM our_outputs " + "WHERE keyindex = ? AND blockheight = ?")); + db_bind_u64(stmt, keyindex); + db_bind_int(stmt, blockheight); + db_query_prepared(stmt); + + while (db_step(stmt)) { + db_col_txid(stmt, "txid", &outpoint.txid); + outpoint.n = db_col_int(stmt, "outnum"); + + watchman_unwatch_outpoint(ld, + owner_wallet_utxo(tmpctx, &outpoint), + &outpoint); + wallet_del_txout_annotation(w, &outpoint); + undo_wallet_add_our_output(w, &outpoint); + wallet_del_tx_if_unreferenced(w, &outpoint.txid); + } + tal_free(stmt); +} diff --git a/wallet/wallet.h b/wallet/wallet.h index 504e49aad91a..c77f4546f8da 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -2022,4 +2022,44 @@ void wallet_datastore_save_payment_description(struct db *db, const char *desc); void migrate_setup_coinmoves(struct lightningd *ld, struct db *db); +/* ==================================================================== + * bwatch-driven wallet recording. + * + * These functions are invoked from lightningd/watchman's dispatch table + * when bwatch reports activity on a wallet-owned scriptpubkey. They + * persist outputs and transactions in the `our_outputs` and `our_txs` + * tables, which run in parallel to the legacy `utxoset` / `transactions` + * tables so a node can downgrade cleanly for one release. + * ==================================================================== */ + +/* Insert a wallet-owned UTXO row into our_outputs. If the same outpoint + * was previously inserted unconfirmed (blockheight=0), the row is updated + * to the new confirmed blockheight so coin selection can spend it. */ +void wallet_add_our_output(struct wallet *w, + const struct bitcoin_outpoint *outpoint, + u32 blockheight, u32 txindex, + const u8 *script, size_t script_len, + struct amount_sat sat, + u32 keyindex); + +/* Insert (or replace) a wallet-relevant transaction in our_txs. */ +void wallet_add_our_tx(struct wallet *w, const struct wally_tx *tx, + u32 blockheight, u32 txindex); + +/* Undo wallet_annotate_txout for an output annotation. */ +void wallet_del_txout_annotation(struct wallet *w, + const struct bitcoin_outpoint *outpoint); + +/* Undo wallet_add_our_tx: removes from our_txs only if no our_outputs row + * still references it. */ +void wallet_del_tx_if_unreferenced(struct wallet *w, + const struct bitcoin_txid *txid); + +/* Shared revert handler for the wallet/p2wpkh, wallet/p2tr and + * wallet/p2sh_p2wpkh dispatch entries: undoes got_utxo + wallet_add_our_tx + * for every output recorded at @suffix's keyindex and @blockheight. */ +void wallet_scriptpubkey_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + #endif /* LIGHTNING_WALLET_WALLET_H */ From b2eb2ea893a4fe466bc7aedf43570cb8976df5d1 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 08:54:51 +0930 Subject: [PATCH 44/77] wallet: add bwatch p2wpkh watch_found handler --- lightningd/watchman.c | 3 ++- lightningd/watchman.h | 3 +++ wallet/wallet.c | 30 ++++++++++++------------------ wallet/wallet.h | 9 +++++++++ 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 06cdf9dcea4b..6000d6173cef 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -475,7 +475,8 @@ static const struct watch_dispatch { watch_found_fn handler; watch_revert_fn revert; } watch_handlers[] = { - /* Entries added in subsequent commits alongside their handler functions. */ + /* wallet/p2wpkh/: WATCH_SCRIPTPUBKEY, fires when a p2wpkh wallet address receives funds */ + { "wallet/p2wpkh/", wallet_watch_p2wpkh, wallet_scriptpubkey_watch_revert }, { NULL, NULL, NULL }, }; diff --git a/lightningd/watchman.h b/lightningd/watchman.h index 445797e6fb2c..4de030a7df27 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -151,4 +151,7 @@ static inline const char *owner_wallet_utxo(const tal_t *ctx, const struct bitcoin_outpoint *op) { return tal_fmt(ctx, "wallet/utxo/%s", fmt_bitcoin_outpoint(ctx, op)); } +static inline const char *owner_wallet_p2wpkh(const tal_t *ctx, u64 keyidx) +{ return tal_fmt(ctx, "wallet/p2wpkh/%"PRIu64, keyidx); } + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ diff --git a/wallet/wallet.c b/wallet/wallet.c index 01fc92b91202..ced55f3c4fbc 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -7999,16 +7999,6 @@ void wallet_add_our_tx(struct wallet *w, const struct wally_tx *tx, * defined earlier in this file * and the legacy one is removed (and this one renamed) once the * chaintopology code path goes away. */ -static void bwatch_got_utxo(struct wallet *w, - u64 keyindex, - enum addrtype addrtype, - const struct wally_tx *wtx, - size_t outnum, - bool is_coinbase, - const u32 *blockheight, - u32 txindex, - struct bitcoin_outpoint *outpoint) - __attribute__((unused)); static void bwatch_got_utxo(struct wallet *w, u64 keyindex, enum addrtype addrtype, @@ -8096,14 +8086,6 @@ static void bwatch_got_utxo(struct wallet *w, * (one each for p2wpkh, p2tr, p2sh_p2wpkh). Cross-checks the matched * output against any pending invoice, records the transaction, and * stores the new UTXO. */ -static void wallet_watch_scriptpubkey_common(struct lightningd *ld, - u32 keyindex, - enum addrtype addrtype, - const struct bitcoin_tx *tx, - size_t outnum, - u32 blockheight, - u32 txindex) - __attribute__((unused)); static void wallet_watch_scriptpubkey_common(struct lightningd *ld, u32 keyindex, enum addrtype addrtype, @@ -8226,3 +8208,15 @@ void wallet_scriptpubkey_watch_revert(struct lightningd *ld, } tal_free(stmt); } + +void wallet_watch_p2wpkh(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex) +{ + wallet_watch_scriptpubkey_common(ld, (u32)strtoull(suffix, NULL, 10), + ADDR_BECH32, + tx, outnum, blockheight, txindex); +} diff --git a/wallet/wallet.h b/wallet/wallet.h index c77f4546f8da..3a49f7e3b2ee 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -2055,6 +2055,15 @@ void wallet_del_txout_annotation(struct wallet *w, void wallet_del_tx_if_unreferenced(struct wallet *w, const struct bitcoin_txid *txid); +/* watch_found handler for the wallet/p2wpkh/ dispatch entry: + * fires when a p2wpkh wallet address receives funds. */ +void wallet_watch_p2wpkh(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex); + /* Shared revert handler for the wallet/p2wpkh, wallet/p2tr and * wallet/p2sh_p2wpkh dispatch entries: undoes got_utxo + wallet_add_our_tx * for every output recorded at @suffix's keyindex and @blockheight. */ From 876e12b7255d22097553234ecb820af00d32d1bb Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 08:57:48 +0930 Subject: [PATCH 45/77] wallet: add bwatch p2tr watch_found handler --- lightningd/watchman.c | 2 ++ lightningd/watchman.h | 3 +++ wallet/wallet.c | 12 ++++++++++++ wallet/wallet.h | 9 +++++++++ 4 files changed, 26 insertions(+) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 6000d6173cef..d5354687191f 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -477,6 +477,8 @@ static const struct watch_dispatch { } watch_handlers[] = { /* wallet/p2wpkh/: WATCH_SCRIPTPUBKEY, fires when a p2wpkh wallet address receives funds */ { "wallet/p2wpkh/", wallet_watch_p2wpkh, wallet_scriptpubkey_watch_revert }, + /* wallet/p2tr/: WATCH_SCRIPTPUBKEY, fires when a p2tr wallet address receives funds */ + { "wallet/p2tr/", wallet_watch_p2tr, wallet_scriptpubkey_watch_revert }, { NULL, NULL, NULL }, }; diff --git a/lightningd/watchman.h b/lightningd/watchman.h index 4de030a7df27..bbc77a8fdcfc 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -154,4 +154,7 @@ static inline const char *owner_wallet_utxo(const tal_t *ctx, static inline const char *owner_wallet_p2wpkh(const tal_t *ctx, u64 keyidx) { return tal_fmt(ctx, "wallet/p2wpkh/%"PRIu64, keyidx); } +static inline const char *owner_wallet_p2tr(const tal_t *ctx, u64 keyidx) +{ return tal_fmt(ctx, "wallet/p2tr/%"PRIu64, keyidx); } + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ diff --git a/wallet/wallet.c b/wallet/wallet.c index ced55f3c4fbc..46132a266324 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -8220,3 +8220,15 @@ void wallet_watch_p2wpkh(struct lightningd *ld, ADDR_BECH32, tx, outnum, blockheight, txindex); } + +void wallet_watch_p2tr(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex) +{ + wallet_watch_scriptpubkey_common(ld, (u32)strtoull(suffix, NULL, 10), + ADDR_P2TR, + tx, outnum, blockheight, txindex); +} diff --git a/wallet/wallet.h b/wallet/wallet.h index 3a49f7e3b2ee..cb8745bda9f2 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -2064,6 +2064,15 @@ void wallet_watch_p2wpkh(struct lightningd *ld, u32 blockheight, u32 txindex); +/* watch_found handler for the wallet/p2tr/ dispatch entry: + * fires when a p2tr wallet address receives funds. */ +void wallet_watch_p2tr(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex); + /* Shared revert handler for the wallet/p2wpkh, wallet/p2tr and * wallet/p2sh_p2wpkh dispatch entries: undoes got_utxo + wallet_add_our_tx * for every output recorded at @suffix's keyindex and @blockheight. */ From 97050d14b892f22c70776dd20dc7be90f8f9580f Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 08:59:29 +0930 Subject: [PATCH 46/77] wallet: add bwatch p2sh_p2wpkh watch_found handler --- lightningd/watchman.c | 2 ++ lightningd/watchman.h | 3 +++ wallet/wallet.c | 12 ++++++++++++ wallet/wallet.h | 9 +++++++++ 4 files changed, 26 insertions(+) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index d5354687191f..136f1a1dd549 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -479,6 +479,8 @@ static const struct watch_dispatch { { "wallet/p2wpkh/", wallet_watch_p2wpkh, wallet_scriptpubkey_watch_revert }, /* wallet/p2tr/: WATCH_SCRIPTPUBKEY, fires when a p2tr wallet address receives funds */ { "wallet/p2tr/", wallet_watch_p2tr, wallet_scriptpubkey_watch_revert }, + /* wallet/p2sh_p2wpkh/: WATCH_SCRIPTPUBKEY, fires when a p2sh-wrapped p2wpkh address receives funds */ + { "wallet/p2sh_p2wpkh/", wallet_watch_p2sh_p2wpkh, wallet_scriptpubkey_watch_revert }, { NULL, NULL, NULL }, }; diff --git a/lightningd/watchman.h b/lightningd/watchman.h index bbc77a8fdcfc..74df5a1d7d14 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -157,4 +157,7 @@ static inline const char *owner_wallet_p2wpkh(const tal_t *ctx, u64 keyidx) static inline const char *owner_wallet_p2tr(const tal_t *ctx, u64 keyidx) { return tal_fmt(ctx, "wallet/p2tr/%"PRIu64, keyidx); } +static inline const char *owner_wallet_p2sh_p2wpkh(const tal_t *ctx, u64 keyidx) +{ return tal_fmt(ctx, "wallet/p2sh_p2wpkh/%"PRIu64, keyidx); } + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ diff --git a/wallet/wallet.c b/wallet/wallet.c index 46132a266324..d680db173e7d 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -8232,3 +8232,15 @@ void wallet_watch_p2tr(struct lightningd *ld, ADDR_P2TR, tx, outnum, blockheight, txindex); } + +void wallet_watch_p2sh_p2wpkh(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex) +{ + wallet_watch_scriptpubkey_common(ld, (u32)strtoull(suffix, NULL, 10), + ADDR_P2SH_SEGWIT, + tx, outnum, blockheight, txindex); +} diff --git a/wallet/wallet.h b/wallet/wallet.h index cb8745bda9f2..b6fe1e3f5150 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -2073,6 +2073,15 @@ void wallet_watch_p2tr(struct lightningd *ld, u32 blockheight, u32 txindex); +/* watch_found handler for the wallet/p2sh_p2wpkh/ dispatch entry: + * fires when a p2sh-wrapped p2wpkh wallet address receives funds. */ +void wallet_watch_p2sh_p2wpkh(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex); + /* Shared revert handler for the wallet/p2wpkh, wallet/p2tr and * wallet/p2sh_p2wpkh dispatch entries: undoes got_utxo + wallet_add_our_tx * for every output recorded at @suffix's keyindex and @blockheight. */ From c1e4885f229bbe840e8332f4086d41ea7a1dbfb0 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 09:13:24 +0930 Subject: [PATCH 47/77] wallet: handle bwatch wallet/utxo spend notifications Add the wallet/utxo/: dispatch entry: on watch_found, mark the UTXO spent in our_outputs, refresh the spending tx in our_txs, and emit a withdrawal coin movement; on watch_revert, clear spendheight so the UTXO becomes unspent again. --- lightningd/watchman.c | 2 + wallet/wallet.c | 88 +++++++++++++++++++++++++++++++++++++++++++ wallet/wallet.h | 19 ++++++++++ 3 files changed, 109 insertions(+) diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 136f1a1dd549..e8e4142297f5 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -475,6 +475,8 @@ static const struct watch_dispatch { watch_found_fn handler; watch_revert_fn revert; } watch_handlers[] = { + /* wallet/utxo/:: WATCH_OUTPOINT, fires when a wallet UTXO is spent */ + { "wallet/utxo/", wallet_utxo_spent_watch_found, wallet_utxo_spent_watch_revert }, /* wallet/p2wpkh/: WATCH_SCRIPTPUBKEY, fires when a p2wpkh wallet address receives funds */ { "wallet/p2wpkh/", wallet_watch_p2wpkh, wallet_scriptpubkey_watch_revert }, /* wallet/p2tr/: WATCH_SCRIPTPUBKEY, fires when a p2tr wallet address receives funds */ diff --git a/wallet/wallet.c b/wallet/wallet.c index d680db173e7d..2dfc33480dd9 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -8244,3 +8245,90 @@ void wallet_watch_p2sh_p2wpkh(struct lightningd *ld, ADDR_P2SH_SEGWIT, tx, outnum, blockheight, txindex); } + +void wallet_record_spend(struct lightningd *ld, + const struct bitcoin_outpoint *outpoint, + const struct bitcoin_txid *txid, + u32 blockheight) +{ + struct utxo *utxo; + + utxo = wallet_utxo_get(tmpctx, ld->wallet, outpoint); + if (!utxo) { + log_broken(ld->log, "No record of utxo %s", + fmt_bitcoin_outpoint(tmpctx, outpoint)); + return; + } + + wallet_save_chain_mvt(ld, new_coin_wallet_withdraw(tmpctx, txid, outpoint, + blockheight, + utxo->amount, + mk_mvt_tags(MVT_WITHDRAWAL))); +} + +void wallet_utxo_spent_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t innum UNUSED, + u32 blockheight, + u32 txindex) +{ + struct bitcoin_outpoint outpoint; + struct db_stmt *stmt; + jsmntok_t tok; + struct bitcoin_txid spending_txid; + + tok.start = 0; + tok.end = strlen(suffix); + if (!json_to_outpoint(suffix, &tok, &outpoint)) { + log_broken(ld->log, "wallet/utxo watch_found: invalid suffix %s", + suffix); + return; + } + + bitcoin_txid(tx, &spending_txid); + + stmt = db_prepare_v2(ld->wallet->db, + SQL("UPDATE our_outputs SET spendheight = ? " + "WHERE txid = ? AND outnum = ?;")); + db_bind_int(stmt, blockheight); + db_bind_txid(stmt, &outpoint.txid); + db_bind_int(stmt, outpoint.n); + db_exec_prepared_v2(take(stmt)); + + /* Refresh the spending tx's confirmed blockheight in our_txs so + * listtransactions reports the correct confirmation. */ + wallet_add_our_tx(ld->wallet, tx->wtx, blockheight, txindex); + + wallet_record_spend(ld, &outpoint, &spending_txid, blockheight); +} + +void wallet_utxo_spent_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight UNUSED) +{ + struct bitcoin_outpoint outpoint; + struct db_stmt *stmt; + jsmntok_t tok; + + tok.start = 0; + tok.end = strlen(suffix); + if (!json_to_outpoint(suffix, &tok, &outpoint)) { + log_broken(ld->log, "wallet/utxo watch_revert: invalid suffix %s", + suffix); + return; + } + + /* Clear spendheight so the UTXO is unspent again. Any coin movement + * already recorded stays; wallet_save_chain_mvt deduplicates if the + * spending tx re-confirms. */ + stmt = db_prepare_v2(ld->wallet->db, + SQL("UPDATE our_outputs SET spendheight = NULL " + "WHERE txid = ? AND outnum = ?;")); + db_bind_txid(stmt, &outpoint.txid); + db_bind_int(stmt, outpoint.n); + db_exec_prepared_v2(take(stmt)); + + log_debug(ld->log, "wallet/utxo watch_revert: cleared spendheight for %s", + fmt_bitcoin_outpoint(tmpctx, &outpoint)); +} diff --git a/wallet/wallet.h b/wallet/wallet.h index b6fe1e3f5150..1addc06a964e 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -2089,4 +2089,23 @@ void wallet_scriptpubkey_watch_revert(struct lightningd *ld, const char *suffix, u32 blockheight); +/* Emit a withdrawal coin movement for a spent wallet UTXO. */ +void wallet_record_spend(struct lightningd *ld, + const struct bitcoin_outpoint *outpoint, + const struct bitcoin_txid *txid, + u32 blockheight); + +/* watch_found handler for wallet/utxo/:. */ +void wallet_utxo_spent_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t innum, + u32 blockheight, + u32 txindex); + +/* Reorg-time counterpart: clears spendheight on the UTXO. */ +void wallet_utxo_spent_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + #endif /* LIGHTNING_WALLET_WALLET_H */ From 29470fab5f149c2ef4b3eb523cea7453de0fa1cd Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 09:57:04 +0930 Subject: [PATCH 48/77] wallet: watch change scriptpubkey on unconfirmed UTXOs In bwatch_got_utxo, register a perennial scriptpubkey watch on every unconfirmed change output via the typed owner_wallet_p2wpkh / owner_wallet_p2tr constructors, so we still see the confirmation notification. The watch uses UINT32_MAX as the start_block sentinel so bwatch keeps it perennially armed: never skip on reorg, never rescan. --- wallet/wallet.c | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/wallet/wallet.c b/wallet/wallet.c index 2dfc33480dd9..c53b565f2a3d 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -7917,23 +7917,32 @@ void migrate_remove_chain_moves_duplicates(struct lightningd *ld, struct db *db) * entries that wire them in are added in follow-on patches in this series. * ==================================================================== */ -/* Map an addrtype to the string used in bwatch owner identifiers, e.g. - * ADDR_BECH32 -> "p2wpkh" giving "wallet/p2wpkh/". */ -static const char *wallet_addrtype_to_owner_prefix(enum addrtype addrtype) - __attribute__((unused)); -static const char *wallet_addrtype_to_owner_prefix(enum addrtype addrtype) +/* Watch this scriptpubkey so bwatch tells us when the output confirms. + * P2SH-P2WPKH change isn't issued by the wallet, so it's a no-op. */ +static void watch_change_scriptpubkey(struct lightningd *ld, + enum addrtype addrtype, + u64 keyindex, + const u8 *script, + size_t script_len) { + const char *owner = NULL; + switch (addrtype) { case ADDR_BECH32: - return "p2wpkh"; + owner = owner_wallet_p2wpkh(tmpctx, keyindex); + break; case ADDR_P2TR: - return "p2tr"; + owner = owner_wallet_p2tr(tmpctx, keyindex); + break; case ADDR_P2SH_SEGWIT: - return "p2sh_p2wpkh"; case ADDR_ALL: break; } - return NULL; + if (!owner) + return; + + /* UINT32_MAX = perennial watch: never skip on reorg, never rescan. */ + watchman_watch_scriptpubkey(ld, owner, script, script_len, UINT32_MAX); } /* Insert a wallet-owned UTXO row into our_outputs. If the outpoint was @@ -8078,6 +8087,10 @@ static void bwatch_got_utxo(struct wallet *w, ? *blockheight : get_block_height(w->ld->topology)); + if (!blockheight) + watch_change_scriptpubkey(w->ld, addrtype, keyindex, + txout->script, txout->script_len); + wallet_annotate_txout(w, &utxo->outpoint, TX_WALLET_DEPOSIT, 0); if (outpoint) *outpoint = utxo->outpoint; From c07ebdbe5a9dc7a6fe194dbf80f17302adf4f036 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 09:59:51 +0930 Subject: [PATCH 49/77] wallet: add wallet_add_bwatch_derkey --- wallet/wallet.c | 20 ++++++++++++++++++++ wallet/wallet.h | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/wallet/wallet.c b/wallet/wallet.c index c53b565f2a3d..5e908cdf510b 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -7945,6 +7945,26 @@ static void watch_change_scriptpubkey(struct lightningd *ld, watchman_watch_scriptpubkey(ld, owner, script, script_len, UINT32_MAX); } +/* Watch the three scriptpubkey forms (p2wpkh, p2sh-p2wpkh, p2tr) of @derkey. */ +void wallet_add_bwatch_derkey(struct lightningd *ld, + u64 keyindex, + u32 start_block, + const u8 derkey[PUBKEY_CMPR_LEN]) +{ + u8 *p2wpkh, *p2sh, *p2tr; + + p2wpkh = scriptpubkey_p2wpkh_derkey(tmpctx, derkey); + p2sh = scriptpubkey_p2sh(tmpctx, p2wpkh); + p2tr = scriptpubkey_p2tr_derkey(tmpctx, derkey); + + watchman_watch_scriptpubkey(ld, owner_wallet_p2wpkh(tmpctx, keyindex), + p2wpkh, tal_bytelen(p2wpkh), start_block); + watchman_watch_scriptpubkey(ld, owner_wallet_p2sh_p2wpkh(tmpctx, keyindex), + p2sh, tal_bytelen(p2sh), start_block); + watchman_watch_scriptpubkey(ld, owner_wallet_p2tr(tmpctx, keyindex), + p2tr, tal_bytelen(p2tr), start_block); +} + /* Insert a wallet-owned UTXO row into our_outputs. If the outpoint was * previously inserted unconfirmed (blockheight=0) and we now have a real * blockheight, promote the row so coin selection can treat it as diff --git a/wallet/wallet.h b/wallet/wallet.h index 1addc06a964e..1646157d9757 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -2108,4 +2108,10 @@ void wallet_utxo_spent_watch_revert(struct lightningd *ld, const char *suffix, u32 blockheight); +/* Watch the three scriptpubkey forms (p2wpkh, p2sh-p2wpkh, p2tr) of @derkey. */ +void wallet_add_bwatch_derkey(struct lightningd *ld, + u64 keyindex, + u32 start_block, + const u8 derkey[PUBKEY_CMPR_LEN]); + #endif /* LIGHTNING_WALLET_WALLET_H */ From c6c2ad904655bbb92a846cc8901ef83d02000c16 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 10:05:06 +0930 Subject: [PATCH 50/77] wallet: add wallet_scriptpubkey_to_keyidx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (ld, db) → keyindex / addrtype lookup over the BIP32 / BIP86 derivation range; lower-level form of wallet_can_spend that doesn't need a fully constructed struct wallet, so migrations (which run before ld->wallet is wired up) can call it. --- wallet/wallet.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++ wallet/wallet.h | 10 ++++++++++ 2 files changed, 57 insertions(+) diff --git a/wallet/wallet.c b/wallet/wallet.c index 5e908cdf510b..a19ba4735403 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -1027,6 +1027,53 @@ bool wallet_add_onchaind_utxo(struct wallet *w, return true; } +bool wallet_scriptpubkey_to_keyidx(struct lightningd *ld, struct db *db, + const u8 *script, size_t script_len, + u32 *index, enum addrtype *addrtype) +{ + /* How far we've derived so far; scan up to this index. */ + u64 max_keyidx = db_get_intvar(db, + ld->bip86_base + ? "bip86_max_index" : "bip32_max_index", + 0); + + for (u64 keyidx = 0; keyidx <= max_keyidx; keyidx++) { + struct pubkey pubkey; + const u8 *p2wpkh, *p2tr, *p2sh; + + if (ld->bip86_base) + bip86_pubkey(ld, &pubkey, (u32)keyidx); + else + bip32_pubkey(ld, &pubkey, (u32)keyidx); + + p2wpkh = scriptpubkey_p2wpkh(tmpctx, &pubkey); + if (tal_bytelen(p2wpkh) == script_len && + memcmp(p2wpkh, script, script_len) == 0) { + if (index) *index = (u32)keyidx; + if (addrtype) *addrtype = ADDR_BECH32; + return true; + } + + p2tr = scriptpubkey_p2tr(tmpctx, &pubkey); + if (tal_bytelen(p2tr) == script_len && + memcmp(p2tr, script, script_len) == 0) { + if (index) *index = (u32)keyidx; + if (addrtype) *addrtype = ADDR_P2TR; + return true; + } + + p2sh = scriptpubkey_p2sh(tmpctx, p2wpkh); + if (tal_bytelen(p2sh) == script_len && + memcmp(p2sh, script, script_len) == 0) { + if (index) *index = (u32)keyidx; + if (addrtype) *addrtype = ADDR_P2SH_SEGWIT; + return true; + } + } + + return false; +} + bool wallet_can_spend(struct wallet *w, const u8 *script, size_t script_len, u32 *index, enum addrtype *addrtype) { diff --git a/wallet/wallet.h b/wallet/wallet.h index 1646157d9757..3012b946c4fb 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -590,6 +590,16 @@ struct utxo **wallet_utxo_boost(const tal_t *ctx, size_t *weight, bool *insufficient); +/** + * wallet_scriptpubkey_to_keyidx - Derive HD keyindex for a scriptpubkey. + * + * Lower-level form that takes (ld, db) directly instead of a wallet. + * Callable from migrations, where ld->wallet is not yet initialised. + */ +bool wallet_scriptpubkey_to_keyidx(struct lightningd *ld, struct db *db, + const u8 *script, size_t script_len, + u32 *index, enum addrtype *addrtype); + /** * wallet_can_spend - Do we have the private key matching this scriptpubkey? * From fe60fe00821e09ec9f439bc27bbdc5dab2254688 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 10:21:51 +0930 Subject: [PATCH 51/77] wallet: add migrate_backfill_bwatch_tables Walk legacy utxoset and back-populate our_outputs with the wallet-owned rows (skipping outputs that don't derive from any HD key, since those are channel funding outputs or gossip watches, not wallet UTXOs), then bulk-copy transactions into our_txs. This release stops updating utxoset/transactions but leaves the rows in place, so no revert is needed: a downgraded binary just resumes from the height the legacy tables were frozen at. --- wallet/migrations.c | 82 +++++++++++++++++++++++++++++++++++++++++++++ wallet/migrations.h | 1 + 2 files changed, 83 insertions(+) diff --git a/wallet/migrations.c b/wallet/migrations.c index d76a298c7ac3..1e4969fd29e5 100644 --- a/wallet/migrations.c +++ b/wallet/migrations.c @@ -10,6 +10,7 @@ #include #include #include +#include static const char *revert_too_early(const tal_t *ctx, struct db *db) { @@ -52,6 +53,83 @@ static const char *revert_withheld_column(const tal_t *ctx, struct db *db) return NULL; } +/* Backfill the new bwatch-driven tables (our_outputs, our_txs) from the + * legacy utxoset / transactions tables, so the bwatch path sees pre-existing + * wallet UTXOs and txs without needing a full rescan. Outputs that don't + * derive from any HD key are skipped: they're channel funding outputs or + * gossip watches, not wallet UTXOs. */ +void migrate_backfill_bwatch_tables(struct lightningd *ld, struct db *db) +{ + struct db_stmt *stmt; + + stmt = db_prepare_v2(db, + SQL("SELECT txid, outnum, blockheight, txindex, " + " scriptpubkey, satoshis, spendheight " + "FROM utxoset " + "WHERE blockheight IS NOT NULL " + " AND scriptpubkey IS NOT NULL " + " AND satoshis IS NOT NULL;")); + db_query_prepared(stmt); + + while (db_step(stmt)) { + struct db_stmt *ins; + struct bitcoin_txid txid; + u32 outnum, blockheight; + struct amount_sat sat; + const u8 *script = db_col_arr(tmpctx, stmt, "scriptpubkey", u8); + size_t script_len = tal_bytelen(script); + u32 keyindex; + + db_col_txid(stmt, "txid", &txid); + outnum = db_col_int(stmt, "outnum"); + blockheight = db_col_int(stmt, "blockheight"); + sat = db_col_amount_sat(stmt, "satoshis"); + + /* TODO: also backfill channel-close outputs (delayed-payment, + * to_remote, anchors) — those use per-channel keys that won't + * match wallet_scriptpubkey_to_keyidx, and need their + * channel_dbid/peer_id/commitment_point/csv columns copied + * straight across from the legacy outputs table. */ + if (!wallet_scriptpubkey_to_keyidx(ld, db, + script, script_len, + &keyindex, NULL)) { + db_col_ignore(stmt, "txindex"); + db_col_ignore(stmt, "spendheight"); + continue; + } + + ins = db_prepare_v2(db, + SQL("INSERT OR IGNORE INTO our_outputs " + "(txid, outnum, blockheight, txindex, " + " scriptpubkey, satoshis, spendheight, keyindex) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?);")); + db_bind_txid(ins, &txid); + db_bind_int(ins, outnum); + db_bind_int(ins, blockheight); + if (db_col_is_null(stmt, "txindex")) + db_bind_null(ins); + else + db_bind_int(ins, db_col_int(stmt, "txindex")); + db_bind_blob(ins, script, script_len); + db_bind_amount_sat(ins, sat); + if (db_col_is_null(stmt, "spendheight")) + db_bind_null(ins); + else + db_bind_int(ins, db_col_int(stmt, "spendheight")); + db_bind_int(ins, keyindex); + db_exec_prepared_v2(take(ins)); + } + tal_free(stmt); + + stmt = db_prepare_v2(db, + SQL("INSERT OR IGNORE INTO our_txs " + "(txid, blockheight, txindex, rawtx) " + "SELECT id, blockheight, txindex, rawtx " + "FROM transactions " + "WHERE blockheight IS NOT NULL AND rawtx IS NOT NULL;")); + db_exec_prepared_v2(take(stmt)); +} + /* Do not reorder or remove elements from this array, it is used to * migrate existing databases from a previous state, based on the * string indices */ @@ -1113,6 +1191,10 @@ static const struct db_migration dbmigrations[] = { " rawtx BLOB" ")"), NULL, SQL("DROP TABLE our_txs"), NULL}, + /* This release stops updating utxoset/transactions + * but leaves the rows in place, so a downgraded binary just resumes + * from the height they were frozen at. */ + {NULL, migrate_backfill_bwatch_tables, NULL, NULL}, }; const struct db_migration *get_db_migrations(size_t *num) diff --git a/wallet/migrations.h b/wallet/migrations.h index dd11f0033e06..7105bae4842e 100644 --- a/wallet/migrations.h +++ b/wallet/migrations.h @@ -63,4 +63,5 @@ void migrate_from_account_db(struct lightningd *ld, struct db *db); void migrate_datastore_commando_runes(struct lightningd *ld, struct db *db); void migrate_runes_idfix(struct lightningd *ld, struct db *db); void migrate_fix_payments_faildetail_type(struct lightningd *ld, struct db *db); +void migrate_backfill_bwatch_tables(struct lightningd *ld, struct db *db); #endif /* LIGHTNING_WALLET_MIGRATIONS_H */ From 6c81b1a04e6ea784cd707fcfa782903ef2b53577 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 10:49:40 +0930 Subject: [PATCH 52/77] lightningd: instantiate watchman and register wallet watches at startup Add init_wallet_scriptpubkey_watches in wallet.c that walks every HD key (BIP32 + BIP86) up to {bip32,bip86}_max_index + keyscan_gap and registers a bwatch watch for each. Call it from main() right after setup_topology, alongside watchman_new. --- lightningd/lightningd.c | 13 +++++++++++++ wallet/wallet.c | 35 +++++++++++++++++++++++++++++++++++ wallet/wallet.h | 6 ++++++ 3 files changed, 54 insertions(+) diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 8847e6637bfa..1ed5441cdb58 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -75,8 +75,10 @@ #include #include #include +#include #include #include +#include #include static void destroy_alt_subdaemons(struct lightningd *ld); @@ -1331,6 +1333,17 @@ 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. Must come before + * init_wallet_scriptpubkey_watches so the watches have somewhere to + * enqueue, and after setup_topology so start_block reflects the + * last-processed height. */ + ld->watchman = watchman_new(ld, ld); + + trace_span_start("init_wallet_scriptpubkey_watches", ld->wallet); + init_wallet_scriptpubkey_watches(ld->wallet, ld->bip32_base); + trace_span_end(ld->wallet); + db_begin_transaction(ld->wallet->db); trace_span_start("delete_old_htlcs", ld->wallet); wallet_delete_old_htlcs(ld->wallet); diff --git a/wallet/wallet.c b/wallet/wallet.c index a19ba4735403..8b52cf22d9da 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -8012,6 +8012,41 @@ void wallet_add_bwatch_derkey(struct lightningd *ld, p2tr, tal_bytelen(p2tr), start_block); } +/* Walk every HD key we've ever derived (plus a keyscan_gap lookahead) and + * register a bwatch watch for it. start_block tells bwatch to (re)scan from + * that height; on a fresh node where topology has no tip yet, get_block_height + * returns 0 and we use UINT32_MAX to mean "watch forever, never rescan". */ +void init_wallet_scriptpubkey_watches(struct wallet *w, + const struct ext_key *bip32_base) +{ + struct ext_key ext; + u64 bip32_max_index = db_get_intvar(w->db, "bip32_max_index", 0); + u64 bip86_max_index; + u32 tip = get_block_height(w->ld->topology); + u32 start_block = tip ? tip : UINT32_MAX; + + for (u64 i = 0; i <= bip32_max_index + w->keyscan_gap; i++) { + if (bip32_key_from_parent(bip32_base, i, + BIP32_FLAG_KEY_PUBLIC, &ext) + != WALLY_OK) { + abort(); + } + wallet_add_bwatch_derkey(w->ld, i, start_block, ext.pub_key); + } + + if (w->ld->bip86_base) { + bip86_max_index = db_get_intvar(w->db, "bip86_max_index", 0); + for (u64 i = 0; i <= bip86_max_index + w->keyscan_gap; i++) { + struct pubkey pubkey; + u8 derkey[PUBKEY_CMPR_LEN]; + + bip86_pubkey(w->ld, &pubkey, i); + pubkey_to_der(derkey, &pubkey); + wallet_add_bwatch_derkey(w->ld, i, start_block, derkey); + } + } +} + /* Insert a wallet-owned UTXO row into our_outputs. If the outpoint was * previously inserted unconfirmed (blockheight=0) and we now have a real * blockheight, promote the row so coin selection can treat it as diff --git a/wallet/wallet.h b/wallet/wallet.h index 3012b946c4fb..495370ab57e3 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -19,6 +19,7 @@ struct amount_msat; struct bitcoin_signature; +struct ext_key; struct invoices; struct channel; struct channel_inflight; @@ -2124,4 +2125,9 @@ void wallet_add_bwatch_derkey(struct lightningd *ld, u32 start_block, const u8 derkey[PUBKEY_CMPR_LEN]); +/* Register bwatch watches for every HD key (BIP32, plus BIP86 if enabled) + * up through {bip32,bip86}_max_index + keyscan_gap. */ +void init_wallet_scriptpubkey_watches(struct wallet *w, + const struct ext_key *bip32_base); + #endif /* LIGHTNING_WALLET_WALLET_H */ From c44388c0e926c47a8c695608abaf16fe3ea31e8a Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 10:58:18 +0930 Subject: [PATCH 53/77] tests: skip all integration tests during bwatch migration TEMPORARY: bwatch replaces chaintopology, watch.c, and txfilter, so most pytests in tests/ will fail mid-migration. Suites are re-enabled file-by-file in later commits as each chaintopology callback gets ported over. --- tests/conftest.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index bd0b7a309275..107a4d8a968d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,6 +28,19 @@ def pytest_configure(config): "openchannel: Limit this test to only run 'v1' or 'v2' openchannel protocol") +def pytest_collection_modifyitems(config, items): + """TEMPORARY: skip all integration tests during the bwatch migration. + + bwatch replaces chaintopology, watch.c, and txfilter, so most pytests + will fail mid-migration. Suites are re-enabled file-by-file in later + commits as each chaintopology callback gets ported over. Remove this + hook once everything is back on. + """ + skip_marker = pytest.mark.skip(reason="bwatch migration in progress") + for item in items: + item.add_marker(skip_marker) + + def pytest_runtest_setup(item): open_versions = [mark.args[0] for mark in item.iter_markers(name='openchannel')] if open_versions: From 54ed3ab90a9a535e965eded15d9ffbcd77d5f949 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 14:07:03 +0930 Subject: [PATCH 54/77] tests: refresh autogenerated mocks for bwatch + watchman migration --- lightningd/lightningd.c | 1 - lightningd/test/run-find_my_abspath.c | 7 +++++ tools/lightning-downgrade.c | 6 +++++ .../test/run-chain_moves_duplicate-detect.c | 27 +++++++++++++++++++ wallet/test/run-db.c | 27 +++++++++++++++++++ ...un-migrate_remove_chain_moves_duplicates.c | 27 +++++++++++++++++++ wallet/test/run-wallet.c | 24 +++++++++++++++++ 7 files changed, 118 insertions(+), 1 deletion(-) diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 1ed5441cdb58..5bedbc88b1e4 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -78,7 +78,6 @@ #include #include #include -#include #include static void destroy_alt_subdaemons(struct lightningd *ld); diff --git a/lightningd/test/run-find_my_abspath.c b/lightningd/test/run-find_my_abspath.c index 3d227bd9974d..2a41dd720fda 100644 --- a/lightningd/test/run-find_my_abspath.c +++ b/lightningd/test/run-find_my_abspath.c @@ -92,6 +92,10 @@ void htlcs_notify_new_block(struct lightningd *ld UNNEEDED) void htlcs_resubmit(struct lightningd *ld UNNEEDED, struct htlc_in_map *unconnected_htlcs_in STEALS UNNEEDED) { fprintf(stderr, "htlcs_resubmit called!\n"); abort(); } +/* Generated stub for init_wallet_scriptpubkey_watches */ +void init_wallet_scriptpubkey_watches(struct wallet *w UNNEEDED, + const struct ext_key *bip32_base UNNEEDED) +{ fprintf(stderr, "init_wallet_scriptpubkey_watches called!\n"); abort(); } /* Generated stub for invoices_start_expiration */ void invoices_start_expiration(struct lightningd *ld UNNEEDED) { fprintf(stderr, "invoices_start_expiration called!\n"); abort(); } @@ -222,6 +226,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/tools/lightning-downgrade.c b/tools/lightning-downgrade.c index 25a118de04e6..4d39e2c007cd 100644 --- a/tools/lightning-downgrade.c +++ b/tools/lightning-downgrade.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #define ERROR_DBVERSION 1 @@ -407,4 +408,9 @@ void migrate_remove_chain_moves_duplicates(struct lightningd *ld UNNEEDED, struc /* Generated stub for migrate_runes_idfix */ void migrate_runes_idfix(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED) { fprintf(stderr, "migrate_runes_idfix called!\n"); abort(); } +/* Generated stub for wallet_scriptpubkey_to_keyidx */ +bool wallet_scriptpubkey_to_keyidx(struct lightningd *ld UNNEEDED, struct db *db UNNEEDED, + const u8 *script UNNEEDED, size_t script_len UNNEEDED, + u32 *index UNNEEDED, enum addrtype *addrtype UNNEEDED) +{ fprintf(stderr, "wallet_scriptpubkey_to_keyidx called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ diff --git a/wallet/test/run-chain_moves_duplicate-detect.c b/wallet/test/run-chain_moves_duplicate-detect.c index 491ccc1da265..cc2797950bd3 100644 --- a/wallet/test/run-chain_moves_duplicate-detect.c +++ b/wallet/test/run-chain_moves_duplicate-detect.c @@ -111,6 +111,9 @@ bool fromwire_hsmd_get_channel_basepoints_reply(const void *p UNNEEDED, struct b /* Generated stub for fromwire_hsmd_get_output_scriptpubkey_reply */ bool fromwire_hsmd_get_output_scriptpubkey_reply(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, u8 **script UNNEEDED) { fprintf(stderr, "fromwire_hsmd_get_output_scriptpubkey_reply called!\n"); abort(); } +/* Generated stub for get_block_height */ +u32 get_block_height(const struct chain_topology *topo UNNEEDED) +{ fprintf(stderr, "get_block_height called!\n"); abort(); } /* Generated stub for get_channel_basepoints */ void get_channel_basepoints(struct lightningd *ld UNNEEDED, const struct node_id *peer_id UNNEEDED, @@ -157,6 +160,12 @@ void inflight_set_last_tx(struct channel_inflight *inflight UNNEEDED, struct bitcoin_tx *last_tx STEALS UNNEEDED, const struct bitcoin_signature last_sig UNNEEDED) { fprintf(stderr, "inflight_set_last_tx called!\n"); abort(); } +/* Generated stub for invoice_check_onchain_payment */ +void invoice_check_onchain_payment(struct lightningd *ld UNNEEDED, + const u8 *scriptPubKey UNNEEDED, + struct amount_sat sat UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED) +{ fprintf(stderr, "invoice_check_onchain_payment called!\n"); abort(); } /* Generated stub for invoices_new */ struct invoices *invoices_new(const tal_t *ctx UNNEEDED, struct wallet *wallet UNNEEDED, @@ -368,6 +377,24 @@ const char *wait_index_name(enum wait_index index UNNEEDED) /* Generated stub for wait_subsystem_name */ const char *wait_subsystem_name(enum wait_subsystem subsystem UNNEEDED) { fprintf(stderr, "wait_subsystem_name called!\n"); abort(); } +/* Generated stub for watchman_unwatch_outpoint */ +void watchman_unwatch_outpoint(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED) +{ fprintf(stderr, "watchman_unwatch_outpoint called!\n"); abort(); } +/* Generated stub for watchman_watch_outpoint */ +void watchman_watch_outpoint(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED, + u32 start_block UNNEEDED) +{ fprintf(stderr, "watchman_watch_outpoint called!\n"); abort(); } +/* Generated stub for watchman_watch_scriptpubkey */ +void watchman_watch_scriptpubkey(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const u8 *scriptpubkey UNNEEDED, + size_t script_len UNNEEDED, + u32 start_block UNNEEDED) +{ fprintf(stderr, "watchman_watch_scriptpubkey called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ void plugin_hook_db_sync(struct db *db UNNEEDED) diff --git a/wallet/test/run-db.c b/wallet/test/run-db.c index 3ac23b20f636..801839c678bb 100644 --- a/wallet/test/run-db.c +++ b/wallet/test/run-db.c @@ -115,6 +115,9 @@ bool fromwire_hsmd_get_channel_basepoints_reply(const void *p UNNEEDED, struct b /* Generated stub for fromwire_hsmd_get_output_scriptpubkey_reply */ bool fromwire_hsmd_get_output_scriptpubkey_reply(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, u8 **script UNNEEDED) { fprintf(stderr, "fromwire_hsmd_get_output_scriptpubkey_reply called!\n"); abort(); } +/* Generated stub for get_block_height */ +u32 get_block_height(const struct chain_topology *topo UNNEEDED) +{ fprintf(stderr, "get_block_height called!\n"); abort(); } /* Generated stub for get_channel_basepoints */ void get_channel_basepoints(struct lightningd *ld UNNEEDED, const struct node_id *peer_id UNNEEDED, @@ -161,6 +164,12 @@ void inflight_set_last_tx(struct channel_inflight *inflight UNNEEDED, struct bitcoin_tx *last_tx STEALS UNNEEDED, const struct bitcoin_signature last_sig UNNEEDED) { fprintf(stderr, "inflight_set_last_tx called!\n"); abort(); } +/* Generated stub for invoice_check_onchain_payment */ +void invoice_check_onchain_payment(struct lightningd *ld UNNEEDED, + const u8 *scriptPubKey UNNEEDED, + struct amount_sat sat UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED) +{ fprintf(stderr, "invoice_check_onchain_payment called!\n"); abort(); } /* Generated stub for invoices_new */ struct invoices *invoices_new(const tal_t *ctx UNNEEDED, struct wallet *wallet UNNEEDED, @@ -381,6 +390,24 @@ const char *wait_index_name(enum wait_index index UNNEEDED) /* Generated stub for wait_subsystem_name */ const char *wait_subsystem_name(enum wait_subsystem subsystem UNNEEDED) { fprintf(stderr, "wait_subsystem_name called!\n"); abort(); } +/* Generated stub for watchman_unwatch_outpoint */ +void watchman_unwatch_outpoint(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED) +{ fprintf(stderr, "watchman_unwatch_outpoint called!\n"); abort(); } +/* Generated stub for watchman_watch_outpoint */ +void watchman_watch_outpoint(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED, + u32 start_block UNNEEDED) +{ fprintf(stderr, "watchman_watch_outpoint called!\n"); abort(); } +/* Generated stub for watchman_watch_scriptpubkey */ +void watchman_watch_scriptpubkey(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const u8 *scriptpubkey UNNEEDED, + size_t script_len UNNEEDED, + u32 start_block UNNEEDED) +{ fprintf(stderr, "watchman_watch_scriptpubkey called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ void plugin_hook_db_sync(struct db *db UNNEEDED) diff --git a/wallet/test/run-migrate_remove_chain_moves_duplicates.c b/wallet/test/run-migrate_remove_chain_moves_duplicates.c index d2f25f8f6cdf..ad0550a6be5f 100644 --- a/wallet/test/run-migrate_remove_chain_moves_duplicates.c +++ b/wallet/test/run-migrate_remove_chain_moves_duplicates.c @@ -145,6 +145,9 @@ bool fromwire_hsmd_get_channel_basepoints_reply(const void *p UNNEEDED, struct b /* Generated stub for fromwire_hsmd_get_output_scriptpubkey_reply */ bool fromwire_hsmd_get_output_scriptpubkey_reply(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, u8 **script UNNEEDED) { fprintf(stderr, "fromwire_hsmd_get_output_scriptpubkey_reply called!\n"); abort(); } +/* Generated stub for get_block_height */ +u32 get_block_height(const struct chain_topology *topo UNNEEDED) +{ fprintf(stderr, "get_block_height called!\n"); abort(); } /* Generated stub for get_channel_basepoints */ void get_channel_basepoints(struct lightningd *ld UNNEEDED, const struct node_id *peer_id UNNEEDED, @@ -194,6 +197,12 @@ void inflight_set_last_tx(struct channel_inflight *inflight UNNEEDED, struct bitcoin_tx *last_tx STEALS UNNEEDED, const struct bitcoin_signature last_sig UNNEEDED) { fprintf(stderr, "inflight_set_last_tx called!\n"); abort(); } +/* Generated stub for invoice_check_onchain_payment */ +void invoice_check_onchain_payment(struct lightningd *ld UNNEEDED, + const u8 *scriptPubKey UNNEEDED, + struct amount_sat sat UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED) +{ fprintf(stderr, "invoice_check_onchain_payment called!\n"); abort(); } /* Generated stub for invoices_new */ struct invoices *invoices_new(const tal_t *ctx UNNEEDED, struct wallet *wallet UNNEEDED, @@ -417,6 +426,24 @@ const char *wait_index_name(enum wait_index index UNNEEDED) /* Generated stub for wait_subsystem_name */ const char *wait_subsystem_name(enum wait_subsystem subsystem UNNEEDED) { fprintf(stderr, "wait_subsystem_name called!\n"); abort(); } +/* Generated stub for watchman_unwatch_outpoint */ +void watchman_unwatch_outpoint(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED) +{ fprintf(stderr, "watchman_unwatch_outpoint called!\n"); abort(); } +/* Generated stub for watchman_watch_outpoint */ +void watchman_watch_outpoint(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED, + u32 start_block UNNEEDED) +{ fprintf(stderr, "watchman_watch_outpoint called!\n"); abort(); } +/* Generated stub for watchman_watch_scriptpubkey */ +void watchman_watch_scriptpubkey(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const u8 *scriptpubkey UNNEEDED, + size_t script_len UNNEEDED, + u32 start_block UNNEEDED) +{ fprintf(stderr, "watchman_watch_scriptpubkey called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ static const char *setup_stmts[] = { SQL("CREATE TABLE vars (" diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index c37517a18ca9..5ad5e3972444 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -390,6 +390,12 @@ void htlc_set_add_(struct lightningd *ld UNNEEDED, void (*succeeded)(void * UNNEEDED, const struct preimage *) UNNEEDED, void *arg UNNEEDED) { fprintf(stderr, "htlc_set_add_ called!\n"); abort(); } +/* Generated stub for invoice_check_onchain_payment */ +void invoice_check_onchain_payment(struct lightningd *ld UNNEEDED, + const u8 *scriptPubKey UNNEEDED, + struct amount_sat sat UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED) +{ fprintf(stderr, "invoice_check_onchain_payment called!\n"); abort(); } /* Generated stub for invoice_check_payment */ const struct invoice_details *invoice_check_payment(const tal_t *ctx UNNEEDED, struct lightningd *ld UNNEEDED, @@ -795,6 +801,24 @@ struct txowatch *watch_txo(const tal_t *ctx UNNEEDED, size_t input_num UNNEEDED, const struct block *block)) { fprintf(stderr, "watch_txo called!\n"); abort(); } +/* Generated stub for watchman_unwatch_outpoint */ +void watchman_unwatch_outpoint(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED) +{ fprintf(stderr, "watchman_unwatch_outpoint called!\n"); abort(); } +/* Generated stub for watchman_watch_outpoint */ +void watchman_watch_outpoint(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED, + u32 start_block UNNEEDED) +{ fprintf(stderr, "watchman_watch_outpoint called!\n"); abort(); } +/* Generated stub for watchman_watch_scriptpubkey */ +void watchman_watch_scriptpubkey(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const u8 *scriptpubkey UNNEEDED, + size_t script_len UNNEEDED, + u32 start_block UNNEEDED) +{ fprintf(stderr, "watchman_watch_scriptpubkey called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ /* Fake stubs to talk to hsm */ From 44216cad6357a3305445164ecad789f4f69032f5 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 14:07:03 +0930 Subject: [PATCH 55/77] lightningd: migrate gossip get_txout to bwatch SCID watches Previously, chaintopology+txfilter watched every P2WSH output in every block and cached them in the wallet utxoset table, all so that gossipd's get_txout requests for a given SCID could be answered locally. bwatch has a native per-SCID watch type, so we can drop the bulk-P2WSH scan and just register a watch for the specific SCIDs gossipd asks about. get_txout now registers a SCID watch with bwatch. When bwatch resolves it, gossip_scid_watch_found replies to gossipd and arms a funding-spent watch on the funding output (or, if the SCID's expected position is empty, tells gossipd the channel is invalid). The two revert handlers (scid + funding-spent) re-arm the SCID watch and undo the earlier get_txout reply if a reorg invalidated it. --- lightningd/gossip_control.c | 245 ++++++++++++++++++++++++------------ lightningd/gossip_control.h | 28 +++++ lightningd/watchman.c | 8 +- lightningd/watchman.h | 11 +- 4 files changed, 211 insertions(+), 81 deletions(-) diff --git a/lightningd/gossip_control.c b/lightningd/gossip_control.c index 9c698cd324b5..72a7d770e925 100644 --- a/lightningd/gossip_control.c +++ b/lightningd/gossip_control.c @@ -13,105 +13,192 @@ #include #include #include +#include -static void got_txout(struct bitcoind *bitcoind, - const struct bitcoin_tx_output *output, - struct short_channel_id scid) +/* Handler for gossipd's WIRE_GOSSIPD_GET_TXOUT request: gossipd has seen a + * channel announcement and wants to verify the funding output exists. We + * register a SCID watch with bwatch; the reply is sent later from + * gossip_scid_watch_found once bwatch confirms (or denies) the output. */ +static void get_txout(struct subd *gossip, const u8 *msg) { - const u8 *script; + struct short_channel_id scid; + u32 blockheight, start_block; + + if (!fromwire_gossipd_get_txout(msg, &scid)) + fatal("Gossip gave bad GOSSIP_GET_TXOUT message %s", + tal_hex(msg, msg)); + + if (gossip->ld->state == LD_STATE_SHUTDOWN) + return; + + /* The SCID tells us which block the channel was confirmed in. Pick + * the lower of (that block, our current tip) as the rescan start: if + * the channel's block is already in the past we want bwatch to rescan + * back to it, but if it's in the future (we're still syncing, or the + * SCID is bogus) we shouldn't ask bwatch to scan a height it hasn't + * reached. */ + blockheight = short_channel_id_blocknum(scid); + start_block = get_block_height(gossip->ld->topology); + if (blockheight < start_block) + start_block = blockheight; + watchman_watch_scid(gossip->ld, + owner_gossip_scid(tmpctx, scid), + &scid, start_block); +} + +/* bwatch has resolved the SCID: either tx!=NULL (funding output confirmed — + * reply to gossipd, then arm the funding-spent watch) or tx==NULL (the SCID's + * block/tx/output position is empty — tell gossipd the channel is invalid). */ +void gossip_scid_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t index, + u32 blockheight, + u32 txindex UNUSED) +{ + struct short_channel_id scid; struct amount_sat sat; + const u8 *script; + struct bitcoin_outpoint outpoint; - /* output will be NULL if it wasn't found */ - if (output) { - script = output->script; - sat = output->amount; - } else { - script = NULL; - sat = AMOUNT_SAT(0); + if (!short_channel_id_from_str(suffix, strlen(suffix), &scid)) { + log_broken(ld->log, + "gossip/: invalid scid suffix '%s'", suffix); + return; + } + + if (!tx) { + /* SCID's expected position absent — tell gossipd it's invalid. */ + log_unusual(ld->log, + "gossip: SCID %s not found at expected" + " block/txindex/outnum — telling gossipd it's invalid", + fmt_short_channel_id(tmpctx, scid)); + if (ld->gossip) { + const u8 *empty = tal_arr(tmpctx, u8, 0); + subd_send_msg(ld->gossip, + take(towire_gossipd_get_txout_reply( + NULL, scid, AMOUNT_SAT(0), empty))); + } + watchman_unwatch_scid(ld, owner_gossip_scid(tmpctx, scid), &scid); + return; } - subd_send_msg( - bitcoind->ld->gossip, - take(towire_gossipd_get_txout_reply(NULL, scid, sat, script))); + if (!ld->gossip) + return; + + sat = bitcoin_tx_output_get_amount_sat(tx, index); + script = tal_dup_arr(tmpctx, u8, + tx->wtx->outputs[index].script, + tx->wtx->outputs[index].script_len, 0); + + subd_send_msg(ld->gossip, + take(towire_gossipd_get_txout_reply(NULL, scid, sat, script))); + + watchman_unwatch_scid(ld, owner_gossip_scid(tmpctx, scid), &scid); + bitcoin_txid(tx, &outpoint.txid); + outpoint.n = index; + watchman_watch_outpoint(ld, + owner_gossip_funding_spent(tmpctx, scid), + &outpoint, blockheight); } -static void got_filteredblock(struct bitcoind *bitcoind, - const struct filteredblock *fb, - struct short_channel_id *scidp) +/* Revert for "gossip/" (WATCH_SCID). The watch is only alive between + * gossipd's get_txout request and SCID confirmation; a revert means the block + * we were waiting for was reorged before the watch fired — nothing was sent + * to gossipd, so just re-arm the watch for when the block returns. */ +void gossip_scid_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight UNUSED) { - struct filteredblock_outpoint *fbo = NULL, *o; - struct bitcoin_tx_output txo; - struct short_channel_id scid = *scidp; - - /* Don't leak this! */ - tal_free(scidp); - - /* If we failed to the filtered block we report the failure to - * got_txout. */ - if (fb == NULL) - return got_txout(bitcoind, NULL, scid); - - /* This routine is mainly for past blocks. As a corner case, - * we will grab (but not save) future blocks if we're - * syncing */ - if (fb->height < bitcoind->ld->topology->root->height) - wallet_filteredblock_add(bitcoind->ld->wallet, fb); - - u32 outnum = short_channel_id_outnum(scid); - u32 txindex = short_channel_id_txnum(scid); - for (size_t i=0; ioutpoints); i++) { - o = fb->outpoints[i]; - if (o->txindex == txindex && o->outpoint.n == outnum) { - fbo = o; - break; - } + struct short_channel_id scid; + + if (!short_channel_id_from_str(suffix, strlen(suffix), &scid)) { + log_broken(ld->log, + "gossip/ revert: invalid scid suffix '%s'", suffix); + return; } - if (fbo) { - txo.amount = fbo->amount; - txo.script = (u8 *)fbo->scriptPubKey; - got_txout(bitcoind, &txo, scid); - } else - got_txout(bitcoind, NULL, scid); + log_unusual(ld->log, + "gossip: SCID %s block reorged before confirmation" + " — re-watching", + fmt_short_channel_id(tmpctx, scid)); + + watchman_watch_scid(ld, + owner_gossip_scid(tmpctx, scid), + &scid, + short_channel_id_blocknum(scid)); } -static void get_txout(struct subd *gossip, const u8 *msg) +void gossip_funding_spent_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx UNUSED, + size_t index UNUSED, + u32 blockheight, + u32 txindex UNUSED) { struct short_channel_id scid; - struct outpoint *op; - u32 blockheight; - struct chain_topology *topo = gossip->ld->topology; - if (!fromwire_gossipd_get_txout(msg, &scid)) - fatal("Gossip gave bad GOSSIP_GET_TXOUT message %s", - tal_hex(msg, msg)); + if (!short_channel_id_from_str(suffix, strlen(suffix), &scid)) { + log_broken(ld->log, + "gossip/funding_spent/: invalid scid suffix '%s'", + suffix); + return; + } - /* FIXME: Block less than 6 deep? */ - blockheight = short_channel_id_blocknum(scid); + if (!ld->gossip) + return; + + gossipd_notify_spends(ld, blockheight, + tal_dup(tmpctx, struct short_channel_id, &scid)); +} + +/* Revert for "gossip/funding_spent/". bwatch reverts in two cases, + * distinguished by blockheight: + * + * funding-block revert (blockheight == scid's block): the SCID's confirming + * block was reorged away, taking the funding output with it. We + * previously sent get_txout_reply so gossipd believes the channel exists + * — undo that. + * + * spend-block revert (blockheight != scid's block): the spending tx was + * reorged; the funding output is unspent again. We previously told + * gossipd the channel was closed — re-arm the SCID watch so gossipd + * re-learns the channel is still open once the funding output + * re-confirms. */ +void gossip_funding_spent_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight) +{ + struct short_channel_id scid; + + if (!short_channel_id_from_str(suffix, strlen(suffix), &scid)) { + log_broken(ld->log, + "gossip/funding_spent/ revert: invalid scid suffix '%s'", + suffix); + return; + } - op = wallet_outpoint_for_scid(tmpctx, gossip->ld->wallet, scid); - if (op) { - subd_send_msg(gossip, - take(towire_gossipd_get_txout_reply( - NULL, scid, op->sat, op->scriptpubkey))); - } else if (wallet_have_block(gossip->ld->wallet, blockheight)) { - /* We should have known about this outpoint since its header - * is in the DB. The fact that we don't means that this is - * either a spent outpoint or an invalid one. Return a - * failure. */ - subd_send_msg(gossip, take(towire_gossipd_get_txout_reply( - NULL, scid, AMOUNT_SAT(0), NULL))); + if (blockheight == short_channel_id_blocknum(scid)) { + log_unusual(ld->log, + "gossip: SCID %s funding block reorged out" + " — notifying gossipd and re-watching", + fmt_short_channel_id(tmpctx, scid)); + if (ld->gossip) + gossipd_notify_spends(ld, blockheight, + tal_dup(tmpctx, + struct short_channel_id, + &scid)); } else { - /* If we're shutting down, don't ask plugins */ - if (gossip->ld->state == LD_STATE_SHUTDOWN) - return; - - /* Make a pointer of a copy of scid here, for got_filteredblock */ - bitcoind_getfilteredblock(topo->bitcoind, topo->bitcoind, - short_channel_id_blocknum(scid), - got_filteredblock, - tal_dup(gossip, struct short_channel_id, &scid)); + log_unusual(ld->log, + "gossip: SCID %s spend reorged out" + " — re-watching for re-confirmation to gossipd", + fmt_short_channel_id(tmpctx, scid)); } + + watchman_watch_scid(ld, + owner_gossip_scid(tmpctx, scid), + &scid, + short_channel_id_blocknum(scid)); } static void handle_init_cupdate(struct lightningd *ld, const u8 *msg) diff --git a/lightningd/gossip_control.h b/lightningd/gossip_control.h index 49baa7fffbe6..19a2d8cdf811 100644 --- a/lightningd/gossip_control.h +++ b/lightningd/gossip_control.h @@ -3,6 +3,7 @@ #include "config.h" #include +struct bitcoin_tx; struct channel; struct lightningd; @@ -14,4 +15,31 @@ void gossipd_notify_spends(struct lightningd *ld, void gossip_notify_new_block(struct lightningd *ld); +/* bwatch handler for "gossip/" (WATCH_SCID). Replies to gossipd's + * pending get_txout request, then arms the funding-spent watch. tx==NULL + * means the SCID's expected position in the block was empty. */ +void gossip_scid_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t index, + u32 blockheight, + u32 txindex); + +void gossip_scid_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + +/* bwatch handler for "gossip/funding_spent/" (WATCH_OUTPOINT). Tells + * gossipd that the channel is closed. */ +void gossip_funding_spent_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t index, + u32 blockheight, + u32 txindex); + +void gossip_funding_spent_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + #endif /* LIGHTNING_LIGHTNINGD_GOSSIP_CONTROL_H */ diff --git a/lightningd/watchman.c b/lightningd/watchman.c index e8e4142297f5..11e6ed081bb1 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -1,7 +1,6 @@ #include "config.h" #include #include -#include #include #include #include @@ -15,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -483,6 +483,12 @@ static const struct watch_dispatch { { "wallet/p2tr/", wallet_watch_p2tr, wallet_scriptpubkey_watch_revert }, /* wallet/p2sh_p2wpkh/: WATCH_SCRIPTPUBKEY, fires when a p2sh-wrapped p2wpkh address receives funds */ { "wallet/p2sh_p2wpkh/", wallet_watch_p2sh_p2wpkh, wallet_scriptpubkey_watch_revert }, + /* gossip/funding_spent/: WATCH_OUTPOINT, fires when the confirmed funding output is spent. + * Must precede "gossip/" so the longer prefix wins the strstarts() match. */ + { "gossip/funding_spent/", gossip_funding_spent_watch_found, gossip_funding_spent_watch_revert }, + /* gossip/: WATCH_SCID, fires when the channel announcement UTXO is confirmed. + * tx==NULL signals the SCID's expected position was absent from the block ("not found"). */ + { "gossip/", gossip_scid_watch_found, gossip_scid_watch_revert }, { NULL, NULL, NULL }, }; diff --git a/lightningd/watchman.h b/lightningd/watchman.h index 74df5a1d7d14..4f3e14be9cee 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -2,6 +2,7 @@ #define LIGHTNING_LIGHTNINGD_WATCHMAN_H #include "config.h" +#include #include #include #include @@ -9,7 +10,6 @@ struct lightningd; struct pending_op; -struct short_channel_id; /* lightningd's view of bwatch. bwatch lives in a separate process and tells * us about new/reverted blocks and watch hits via JSON-RPC; watchman tracks @@ -160,4 +160,13 @@ static inline const char *owner_wallet_p2tr(const tal_t *ctx, u64 keyidx) static inline const char *owner_wallet_p2sh_p2wpkh(const tal_t *ctx, u64 keyidx) { return tal_fmt(ctx, "wallet/p2sh_p2wpkh/%"PRIu64, keyidx); } +/* gossip/ owners */ +static inline const char *owner_gossip_scid(const tal_t *ctx, + struct short_channel_id scid) +{ return tal_fmt(ctx, "gossip/%s", fmt_short_channel_id(ctx, scid)); } + +static inline const char *owner_gossip_funding_spent(const tal_t *ctx, + struct short_channel_id scid) +{ return tal_fmt(ctx, "gossip/funding_spent/%s", fmt_short_channel_id(ctx, scid)); } + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ From d6abc1182594e79ad6b995cef5cd567d9a3c7687 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 15:40:33 +0930 Subject: [PATCH 56/77] lightningd: migrate channel funding scriptpubkey watch to bwatch When a channel's funding output script appears in a confirmed tx, bwatch now calls channel_funding_watch_found. It records the SCID (closing the channel if it doesn't fit) and starts a depth watch so channeld can drive its lock-in state machine. The revert handler is a no-op. All funding-reorg cleanup is owned by the funding-depth watch's revert handler, which lands in the next commit. --- lightningd/peer_control.c | 75 +++++++++++++++------ lightningd/peer_control.h | 15 +++++ lightningd/test/run-invoice-select-inchan.c | 29 ++++---- lightningd/watchman.c | 4 ++ lightningd/watchman.h | 4 ++ wallet/test/run-wallet.c | 6 ++ 6 files changed, 98 insertions(+), 35 deletions(-) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index ddcdb27e9dcf..5003d560d180 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -2436,20 +2437,45 @@ void channel_watch_depth(struct lightningd *ld, channel); } -/* We see this tx output spend to the funding address. */ -static void channel_funding_found(struct lightningd *ld, - const struct bitcoin_tx *tx, - u32 outnum, - const struct txlocator *loc, - struct channel *channel) +void channel_funding_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx UNUSED, + size_t outnum UNUSED, + u32 blockheight, + u32 txindex) { - /* Closes channel if it doesn't fit in an scid! */ - if (depthcb_update_scid(channel, &channel->funding, loc)) { - /* We will almost immediately get called, which is what we want! */ - channel_watch_depth(ld, loc->blkheight, channel); + u64 dbid = strtoull(suffix, NULL, 10); + struct channel *channel = channel_by_dbid(ld, dbid); + struct txlocator loc; + + if (!channel) { + log_broken(ld->log, + "channel/funding watch_found: no channel for dbid %"PRIu64, + dbid); + return; + } + + /* depthcb_update_scid() expects a txlocator; we have the block height + * and txindex directly, so just fill one in on the stack. */ + loc.blkheight = blockheight; + loc.index = txindex; + + /* Closes the channel if the scid doesn't fit. */ + if (depthcb_update_scid(channel, &channel->funding, &loc)) { + /* Will fire depth callbacks immediately, which is what we want. */ + channel_watch_depth(ld, blockheight, channel); } } +/* No-op: the funding-depth watch's revert handler owns all funding-reorg + * logic. Either it has already run (clearing scid), or the channel is + * still AWAITING_LOCKIN with no confirmed state to roll back. */ +void channel_funding_watch_revert(struct lightningd *ld UNUSED, + const char *suffix UNUSED, + u32 blockheight UNUSED) +{ +} + static enum watch_result funding_spent(struct channel *channel, const struct bitcoin_tx *tx, size_t inputnum UNUSED, @@ -2511,13 +2537,17 @@ void channel_watch_funding(struct lightningd *ld, struct channel *channel) const u8 *funding_wscript = bitcoin_redeem_2of2(tmpctx, &channel->local_funding_pubkey, &channel->channel_info.remote_fundingkey); + const u8 *funding_spk = scriptpubkey_p2wsh(tmpctx, funding_wscript); - watch_scriptpubkey(channel, ld->topology, - take(scriptpubkey_p2wsh(NULL, funding_wscript)), - &channel->funding, - channel->funding_sats, - channel_funding_found, - channel); + /* Hand the funding scriptpubkey watch to bwatch. start_block + * is the current tip: rescan any past blocks if we're catching + * up after a restart, otherwise just watch from now. */ + watchman_watch_scriptpubkey(ld, + owner_channel_funding(tmpctx, + channel->dbid), + funding_spk, + tal_bytelen(funding_spk), + get_block_height(ld->topology)); } /* We watch for closing of course. */ @@ -2530,17 +2560,18 @@ void channel_unwatch_funding(struct lightningd *ld, struct channel *channel) const u8 *funding_wscript = bitcoin_redeem_2of2(tmpctx, &channel->local_funding_pubkey, &channel->channel_info.remote_fundingkey); + const u8 *funding_spk; /* This is stub channel, we don't watch anything! */ if (channel->scid && is_stub_scid(*channel->scid)) return; - unwatch_scriptpubkey(channel, ld->topology, - scriptpubkey_p2wsh(tmpctx, funding_wscript), - &channel->funding, - channel->funding_sats, - channel_funding_found, - channel); + funding_spk = scriptpubkey_p2wsh(tmpctx, funding_wscript); + watchman_unwatch_scriptpubkey(ld, + owner_channel_funding(tmpctx, + channel->dbid), + funding_spk, + tal_bytelen(funding_spk)); /* FIXME: unwatch txo and depth too? */ } diff --git a/lightningd/peer_control.h b/lightningd/peer_control.h index 279a2f91d679..2e88d184b403 100644 --- a/lightningd/peer_control.h +++ b/lightningd/peer_control.h @@ -138,6 +138,21 @@ void update_channel_from_inflight(struct lightningd *ld, void channel_watch_funding(struct lightningd *ld, struct channel *channel); void channel_unwatch_funding(struct lightningd *ld, struct channel *channel); +/* bwatch handler for "channel/funding/" (WATCH_SCRIPTPUBKEY): the + * funding output script appeared in a tx, so the channel's funding tx has + * been confirmed. Records the SCID and starts a depth watch to drive + * channeld's lock-in state machine. */ +void channel_funding_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t outnum, + u32 blockheight, + u32 txindex); + +void channel_funding_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + /* Watch for spend of funding tx. */ void channel_watch_funding_out(struct lightningd *ld, struct channel *channel); diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index 6c87d155ba5c..a36be19251cd 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -57,6 +57,9 @@ void broadcast_tx_(const tal_t *ctx UNNEEDED, struct channel *channel_by_cid(struct lightningd *ld UNNEEDED, const struct channel_id *cid UNNEEDED) { fprintf(stderr, "channel_by_cid called!\n"); abort(); } +/* Generated stub for channel_by_dbid */ +struct channel *channel_by_dbid(struct lightningd *ld UNNEEDED, const u64 dbid UNNEEDED) +{ fprintf(stderr, "channel_by_dbid called!\n"); abort(); } /* Generated stub for channel_change_state_reason_str */ const char *channel_change_state_reason_str(enum state_change reason UNNEEDED) { fprintf(stderr, "channel_change_state_reason_str called!\n"); abort(); } @@ -658,19 +661,6 @@ u8 *towire_onchaind_dev_memleak(const tal_t *ctx UNNEEDED) /* Generated stub for towire_openingd_dev_memleak */ u8 *towire_openingd_dev_memleak(const tal_t *ctx UNNEEDED) { fprintf(stderr, "towire_openingd_dev_memleak called!\n"); abort(); } -/* Generated stub for unwatch_scriptpubkey_ */ -bool unwatch_scriptpubkey_(const tal_t *ctx UNNEEDED, - struct chain_topology *topo UNNEEDED, - const u8 *scriptpubkey UNNEEDED, - const struct bitcoin_outpoint *expected_outpoint UNNEEDED, - struct amount_sat expected_amount UNNEEDED, - void (*cb)(struct lightningd *ld UNNEEDED, - const struct bitcoin_tx *tx UNNEEDED, - u32 outnum UNNEEDED, - const struct txlocator *loc UNNEEDED, - void *) UNNEEDED, - void *arg UNNEEDED) -{ fprintf(stderr, "unwatch_scriptpubkey_ called!\n"); abort(); } /* Generated stub for wallet_channel_save */ void wallet_channel_save(struct wallet *w UNNEEDED, struct channel *chan UNNEEDED) { fprintf(stderr, "wallet_channel_save called!\n"); abort(); } @@ -778,6 +768,19 @@ struct txowatch *watch_txo(const tal_t *ctx UNNEEDED, size_t input_num UNNEEDED, const struct block *block)) { fprintf(stderr, "watch_txo called!\n"); abort(); } +/* Generated stub for watchman_unwatch_scriptpubkey */ +void watchman_unwatch_scriptpubkey(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const u8 *scriptpubkey UNNEEDED, + size_t script_len UNNEEDED) +{ fprintf(stderr, "watchman_unwatch_scriptpubkey called!\n"); abort(); } +/* Generated stub for watchman_watch_scriptpubkey */ +void watchman_watch_scriptpubkey(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const u8 *scriptpubkey UNNEEDED, + size_t script_len UNNEEDED, + u32 start_block UNNEEDED) +{ fprintf(stderr, "watchman_watch_scriptpubkey called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ static void add_candidate(struct routehint_candidate **candidates, int n, diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 11e6ed081bb1..d77181f4f5e6 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -489,6 +490,9 @@ static const struct watch_dispatch { /* gossip/: WATCH_SCID, fires when the channel announcement UTXO is confirmed. * tx==NULL signals the SCID's expected position was absent from the block ("not found"). */ { "gossip/", gossip_scid_watch_found, gossip_scid_watch_revert }, + /* channel/funding/: WATCH_SCRIPTPUBKEY, fires when the funding output script + * appears in a tx (i.e. the channel's funding transaction has been confirmed). */ + { "channel/funding/", channel_funding_watch_found, channel_funding_watch_revert }, { NULL, NULL, NULL }, }; diff --git a/lightningd/watchman.h b/lightningd/watchman.h index 4f3e14be9cee..34c6682fb640 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -169,4 +169,8 @@ static inline const char *owner_gossip_funding_spent(const tal_t *ctx, struct short_channel_id scid) { return tal_fmt(ctx, "gossip/funding_spent/%s", fmt_short_channel_id(ctx, scid)); } +/* channel/ owners */ +static inline const char *owner_channel_funding(const tal_t *ctx, u64 dbid) +{ return tal_fmt(ctx, "channel/funding/%"PRIu64, dbid); } + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 5ad5e3972444..3c1382a9069e 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -806,6 +806,12 @@ void watchman_unwatch_outpoint(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED) { fprintf(stderr, "watchman_unwatch_outpoint called!\n"); abort(); } +/* Generated stub for watchman_unwatch_scriptpubkey */ +void watchman_unwatch_scriptpubkey(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const u8 *scriptpubkey UNNEEDED, + size_t script_len UNNEEDED) +{ fprintf(stderr, "watchman_unwatch_scriptpubkey called!\n"); abort(); } /* Generated stub for watchman_watch_outpoint */ void watchman_watch_outpoint(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, From 1d28024b22c711f46e5054466147fc13bf8d38c1 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 20:30:33 +0930 Subject: [PATCH 57/77] lightningd: add bwatch funding-depth path Adds the bwatch funding-depth handler trio without wiring it in: channel_funding_depth_found - per-block depth tick from bwatch channel_funding_depth_revert - confirming block reorged away channel_block_processed - per-block fan-out across all channels The handlers update channel->depth, drive channeld_tell_depth, fire lockin_complete once minimum_depth is reached, and unwatch themselves once depth passes max(minimum_depth, ANNOUNCE_MIN_DEPTH). Both paths share a depth-equality skip guard so they can't double-notify channeld. The "channel/funding_depth/" dispatch entry is registered, but no caller yet creates such a watch and channel_block_processed is not yet called from watchman's block-processed RPC, so this commit is a pure addition with no behavior change. channel_watch_depth still drives funding depth via chaintopology's funding_depth_cb / funding_reorged_cb; both get swapped out in the next commit. --- lightningd/peer_control.c | 185 ++++++++++++++++++++ lightningd/peer_control.h | 22 +++ lightningd/test/run-invoice-select-inchan.c | 5 + lightningd/watchman.c | 4 +- lightningd/watchman.h | 3 + wallet/test/run-wallet.c | 5 + 6 files changed, 223 insertions(+), 1 deletion(-) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 5003d560d180..7bff3740ac37 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -2476,6 +2476,191 @@ void channel_funding_watch_revert(struct lightningd *ld UNUSED, { } +void channel_funding_depth_found(struct lightningd *ld, + const char *suffix, + u32 depth, + u32 blockheight) +{ + u64 dbid = strtoull(suffix, NULL, 10); + struct channel *channel = channel_by_dbid(ld, dbid); + u32 stop_depth; + + if (!channel) { + log_debug(ld->log, + "channel/funding_depth: unknown dbid %"PRIu64", ignoring", + dbid); + return; + } + + /* channel_block_processed runs every block too; whichever path + * fires first sets channel->depth and the other sees the same value + * here and skips, avoiding duplicate channeld notifications. */ + if (depth == channel->depth) + return; + + channel->depth = depth; + log_debug(channel->log, + "channel/funding_depth: depth %u at block %u (state %s)", + depth, blockheight, channel_state_name(channel)); + + switch (channel->state) { + case CHANNELD_AWAITING_LOCKIN: + channeld_tell_depth(channel, &channel->funding.txid, depth); + if (depth >= channel->minimum_depth + && channel->remote_channel_ready) + lockin_complete(channel, CHANNELD_AWAITING_LOCKIN); + break; + case CHANNELD_NORMAL: + case CHANNELD_AWAITING_SPLICE: + channeld_tell_depth(channel, &channel->funding.txid, depth); + break; + default: + /* DUALOPEND_AWAITING_LOCKIN, ONCHAIN, etc. are driven by their + * own watches today. */ + break; + } + + /* Stop the depth watch once both lock-in and gossip-announce + * thresholds are satisfied; further depth ticks are unnecessary. */ + stop_depth = (channel->minimum_depth > ANNOUNCE_MIN_DEPTH) + ? channel->minimum_depth : ANNOUNCE_MIN_DEPTH; + if (depth >= stop_depth) { + u32 confirm_height = blockheight - depth + 1; + watchman_unwatch_blockdepth(ld, + owner_channel_funding_depth(tmpctx, dbid), + confirm_height); + } +} + +void channel_funding_depth_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight) +{ + u64 dbid = strtoull(suffix, NULL, 10); + struct channel *channel = channel_by_dbid(ld, dbid); + + if (!channel) { + log_debug(ld->log, + "channel/funding_depth revert: unknown dbid %"PRIu64", ignoring", + dbid); + watchman_unwatch_blockdepth(ld, + owner_channel_funding_depth(tmpctx, dbid), + blockheight); + return; + } + + if (!channel->scid || is_stub_scid(*channel->scid)) + return; + + log_unusual(channel->log, + "Funding tx REORG from depth %u (state %s)", + channel->depth, channel_state_name(channel)); + channel->depth = 0; + + watchman_unwatch_blockdepth(ld, + owner_channel_funding_depth(tmpctx, dbid), + blockheight); + + switch (channel->state) { + case DUALOPEND_AWAITING_LOCKIN: + case DUALOPEND_OPEN_INIT: + case DUALOPEND_OPEN_COMMIT_READY: + case DUALOPEND_OPEN_COMMITTED: + channel_internal_error(channel, + "Bad %s state: %s", + __func__, + channel_state_name(channel)); + return; + case CHANNELD_AWAITING_LOCKIN: + channel_set_scid(channel, NULL); + return; + case CHANNELD_AWAITING_SPLICE: + case CHANNELD_NORMAL: + if (channel->opener == LOCAL || channel->minimum_depth == 0) { + channel_fail_transient(channel, true, + "Funding tx %s reorganized out, but %s...", + fmt_bitcoin_txid(tmpctx, &channel->funding.txid), + channel->opener == LOCAL + ? "we opened it" + : "zeroconf anyway"); + return; + } + /* fall through */ + case AWAITING_UNILATERAL: + case CHANNELD_SHUTTING_DOWN: + case CLOSINGD_SIGEXCHANGE: + case CLOSINGD_COMPLETE: + case FUNDING_SPEND_SEEN: + case ONCHAIN: + case CLOSED: + break; + } + + channel_internal_error(channel, + "Funding transaction has been reorged out in state %s", + channel_state_name(channel)); +} + +void channel_block_processed(struct lightningd *ld, u32 blockheight) +{ + struct peer *peer; + struct peer_node_id_map_iter it; + + for (peer = peer_node_id_map_first(ld->peers, &it); + peer; + peer = peer_node_id_map_next(ld->peers, &it)) { + struct channel *channel; + + list_for_each(&peer->channels, channel, list) { + u32 fund_block, depth; + + /* onchaind drives its own per-tx depth tracking. */ + if (channel->state == ONCHAIN + || channel->state == FUNDING_SPEND_SEEN) + continue; + + /* Skip unconfirmed channels; stub scids are zero-conf placeholders. */ + if (!channel->scid || is_stub_scid(*channel->scid)) + continue; + + fund_block = short_channel_id_blocknum(*channel->scid); + depth = (blockheight >= fund_block) + ? (blockheight - fund_block + 1) + : 0; + + if (depth == channel->depth) + continue; + + channel->depth = depth; + log_debug(channel->log, + "Funding depth %u (block %u, scid block %u)", + depth, blockheight, fund_block); + + switch (channel->state) { + case CHANNELD_AWAITING_LOCKIN: + channeld_tell_depth(channel, + &channel->funding.txid, + depth); + if (depth >= channel->minimum_depth + && channel->remote_channel_ready) + lockin_complete(channel, + CHANNELD_AWAITING_LOCKIN); + break; + case CHANNELD_NORMAL: + case CHANNELD_AWAITING_SPLICE: + channeld_tell_depth(channel, + &channel->funding.txid, + depth); + break; + default: + /* DUALOPEND_*, AWAITING_UNILATERAL, CLOSED, etc. + * keep using their existing depth-driving paths. */ + break; + } + } + } +} + static enum watch_result funding_spent(struct channel *channel, const struct bitcoin_tx *tx, size_t inputnum UNUSED, diff --git a/lightningd/peer_control.h b/lightningd/peer_control.h index 2e88d184b403..4c68639e8ae7 100644 --- a/lightningd/peer_control.h +++ b/lightningd/peer_control.h @@ -153,6 +153,28 @@ void channel_funding_watch_revert(struct lightningd *ld, const char *suffix, u32 blockheight); +/* bwatch handler for "channel/funding_depth/" (WATCH_BLOCKDEPTH): fires + * once per new block while the funding tx is accumulating confirmations. + * Drives channeld's depth state machine and triggers lock-in once + * minimum_depth is met. Unwatches itself once depth reaches + * max(minimum_depth, ANNOUNCE_MIN_DEPTH). */ +void channel_funding_depth_found(struct lightningd *ld, + const char *suffix, + u32 depth, + u32 blockheight); + +/* Reorg of the block that confirmed the funding tx: clear scid and, for + * states past lock-in, fail the channel transiently so it reconnects once + * the tx is re-mined. */ +void channel_funding_depth_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + +/* Called from watchman's block_processed handler once per new block. + * Iterates every channel whose funding tx has confirmed and drives its + * depth-dependent state (lock-in, gossip announce, splice). */ +void channel_block_processed(struct lightningd *ld, u32 blockheight); + /* Watch for spend of funding tx. */ void channel_watch_funding_out(struct lightningd *ld, struct channel *channel); diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index a36be19251cd..b3429d13a103 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -768,6 +768,11 @@ struct txowatch *watch_txo(const tal_t *ctx UNNEEDED, size_t input_num UNNEEDED, const struct block *block)) { fprintf(stderr, "watch_txo called!\n"); abort(); } +/* Generated stub for watchman_unwatch_blockdepth */ +void watchman_unwatch_blockdepth(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + u32 confirm_height UNNEEDED) +{ fprintf(stderr, "watchman_unwatch_blockdepth called!\n"); abort(); } /* Generated stub for watchman_unwatch_scriptpubkey */ void watchman_unwatch_scriptpubkey(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, diff --git a/lightningd/watchman.c b/lightningd/watchman.c index d77181f4f5e6..ca5e1304188d 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -467,7 +467,9 @@ static const struct depth_dispatch { depth_found_fn handler; watch_revert_fn revert; } depth_handlers[] = { - /* Entries added in subsequent commits alongside their handler functions. */ + /* channel/funding_depth/: WATCH_BLOCKDEPTH, fires once per new block + * while the funding tx accumulates confirmations. */ + { "channel/funding_depth/", channel_funding_depth_found, channel_funding_depth_revert }, { NULL, NULL, NULL }, }; diff --git a/lightningd/watchman.h b/lightningd/watchman.h index 34c6682fb640..cd23ce0e37a8 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -173,4 +173,7 @@ static inline const char *owner_gossip_funding_spent(const tal_t *ctx, static inline const char *owner_channel_funding(const tal_t *ctx, u64 dbid) { return tal_fmt(ctx, "channel/funding/%"PRIu64, dbid); } +static inline const char *owner_channel_funding_depth(const tal_t *ctx, u64 dbid) +{ return tal_fmt(ctx, "channel/funding_depth/%"PRIu64, dbid); } + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 3c1382a9069e..f2bf28d7d71b 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -801,6 +801,11 @@ struct txowatch *watch_txo(const tal_t *ctx UNNEEDED, size_t input_num UNNEEDED, const struct block *block)) { fprintf(stderr, "watch_txo called!\n"); abort(); } +/* Generated stub for watchman_unwatch_blockdepth */ +void watchman_unwatch_blockdepth(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + u32 confirm_height UNNEEDED) +{ fprintf(stderr, "watchman_unwatch_blockdepth called!\n"); abort(); } /* Generated stub for watchman_unwatch_outpoint */ void watchman_unwatch_outpoint(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, From 4ff200926a3497cf6e56dfa103ca481a39b34580 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Tue, 21 Apr 2026 20:59:33 +0930 Subject: [PATCH 58/77] lightningd: replace funding_depth_cb with bwatch path Switches funding-depth tracking from chaintopology to bwatch. --- lightningd/peer_control.c | 124 +------------------- lightningd/test/run-invoice-select-inchan.c | 13 +- lightningd/watchman.c | 3 +- wallet/test/run-wallet.c | 13 +- 4 files changed, 14 insertions(+), 139 deletions(-) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 7bff3740ac37..238cacb8c3e3 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -2310,131 +2310,13 @@ void update_channel_from_inflight(struct lightningd *ld, wallet_channel_save(ld->wallet, channel); } -/* All reorg callback must return DELETE_WATCH; we make this so it's clear that we - * won't be called again. */ -static enum watch_result funding_reorged_cb(struct lightningd *ld, struct channel *channel) -{ - log_unusual(channel->log, "Funding txid %s REORG from depth %u (state %s)", - fmt_bitcoin_txid(tmpctx, &channel->funding.txid), - channel->depth, - channel_state_name(channel)); - channel->depth = 0; - - /* That's not entirely unexpected in early states */ - switch (channel->state) { - case DUALOPEND_AWAITING_LOCKIN: - case DUALOPEND_OPEN_INIT: - case DUALOPEND_OPEN_COMMIT_READY: - case DUALOPEND_OPEN_COMMITTED: - /* Shouldn't be here! */ - channel_internal_error(channel, - "Bad %s state: %s", - __func__, - channel_state_name(channel)); - return DELETE_WATCH; - case CHANNELD_AWAITING_LOCKIN: - /* That's not entirely unexpected in early states */ - log_debug(channel->log, "Funding tx %s reorganized out!", - fmt_bitcoin_txid(tmpctx, &channel->funding.txid)); - channel_set_scid(channel, NULL); - return DELETE_WATCH; - - /* But it's often Bad News in later states */ - case CHANNELD_AWAITING_SPLICE: - case CHANNELD_NORMAL: - /* If we opened, or it's zero-conf, we trust them anyway. */ - if (channel->opener == LOCAL - || channel->minimum_depth == 0) { - const char *str; - - str = tal_fmt(tmpctx, - "Funding tx %s reorganized out, but %s...", - fmt_bitcoin_txid(tmpctx, &channel->funding.txid), - channel->opener == LOCAL ? "we opened it" : "zeroconf anyway"); - - /* Log even if not connected! */ - if (!channel->owner) - log_info(channel->log, "%s", str); - channel_fail_transient(channel, true, "%s", str); - return DELETE_WATCH; - } - /* fall thru */ - case AWAITING_UNILATERAL: - case CHANNELD_SHUTTING_DOWN: - case CLOSINGD_SIGEXCHANGE: - case CLOSINGD_COMPLETE: - case FUNDING_SPEND_SEEN: - case ONCHAIN: - case CLOSED: - break; - } - - channel_internal_error(channel, - "Funding transaction has been reorged out in state %s!", - channel_state_name(channel)); - return DELETE_WATCH; -} - -static enum watch_result funding_depth_cb(struct lightningd *ld, - unsigned int depth, - struct channel *channel) -{ - channel->depth = depth; - - log_debug(channel->log, "Funding tx %s depth %u of %u", - fmt_bitcoin_txid(tmpctx, &channel->funding.txid), - depth, channel->minimum_depth); - - switch (channel->state) { - /* We should not be in the callback! */ - case DUALOPEND_AWAITING_LOCKIN: - case DUALOPEND_OPEN_INIT: - case DUALOPEND_OPEN_COMMIT_READY: - case DUALOPEND_OPEN_COMMITTED: - abort(); - - case AWAITING_UNILATERAL: - case CHANNELD_SHUTTING_DOWN: - case CLOSINGD_SIGEXCHANGE: - case CLOSINGD_COMPLETE: - case FUNDING_SPEND_SEEN: - case ONCHAIN: - case CLOSED: - /* If not awaiting lockin/announce, it doesn't care any more */ - log_debug(channel->log, - "Funding tx %s confirmed, but peer in state %s", - fmt_bitcoin_txid(tmpctx, &channel->funding.txid), - channel_state_name(channel)); - return DELETE_WATCH; - - case CHANNELD_AWAITING_LOCKIN: - /* This may be redundant, and may be public later, but - * make sure we tell gossipd at least once */ - if (depth >= channel->minimum_depth - && channel->remote_channel_ready) { - lockin_complete(channel, CHANNELD_AWAITING_LOCKIN); - } - /* Fall thru */ - case CHANNELD_NORMAL: - case CHANNELD_AWAITING_SPLICE: - channeld_tell_depth(channel, &channel->funding.txid, depth); - - if (depth < ANNOUNCE_MIN_DEPTH || depth < channel->minimum_depth) - return KEEP_WATCHING; - /* Normal state and past announce depth? Stop bothering us! */ - return DELETE_WATCH; - } - abort(); -} - void channel_watch_depth(struct lightningd *ld, u32 blockheight, struct channel *channel) { - watch_blockdepth(channel, ld->topology, blockheight, - funding_depth_cb, - funding_reorged_cb, - channel); + watchman_watch_blockdepth(ld, + owner_channel_funding_depth(tmpctx, channel->dbid), + blockheight); } void channel_funding_watch_found(struct lightningd *ld, diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index b3429d13a103..d712672f755e 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -729,14 +729,6 @@ void wallet_unreserve_utxo(struct wallet *w UNNEEDED, struct utxo *utxo UNNEEDED struct utxo *wallet_utxo_get(const tal_t *ctx UNNEEDED, struct wallet *w UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED) { fprintf(stderr, "wallet_utxo_get called!\n"); abort(); } -/* Generated stub for watch_blockdepth_ */ -bool watch_blockdepth_(const tal_t *ctx UNNEEDED, - struct chain_topology *topo UNNEEDED, - u32 blockheight UNNEEDED, - enum watch_result (*depthcb)(struct lightningd *ld UNNEEDED, u32 depth UNNEEDED, void *) UNNEEDED, - enum watch_result (*reorgcb)(struct lightningd *ld UNNEEDED, void *) UNNEEDED, - void *arg UNNEEDED) -{ fprintf(stderr, "watch_blockdepth_ called!\n"); abort(); } /* Generated stub for watch_opening_inflight */ void watch_opening_inflight(struct lightningd *ld UNNEEDED, struct channel_inflight *inflight UNNEEDED) @@ -779,6 +771,11 @@ void watchman_unwatch_scriptpubkey(struct lightningd *ld UNNEEDED, const u8 *scriptpubkey UNNEEDED, size_t script_len UNNEEDED) { fprintf(stderr, "watchman_unwatch_scriptpubkey called!\n"); abort(); } +/* Generated stub for watchman_watch_blockdepth */ +void watchman_watch_blockdepth(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + u32 confirm_height UNNEEDED) +{ fprintf(stderr, "watchman_watch_blockdepth called!\n"); abort(); } /* Generated stub for watchman_watch_scriptpubkey */ void watchman_watch_scriptpubkey(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, diff --git a/lightningd/watchman.c b/lightningd/watchman.c index ca5e1304188d..be1f4b482146 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -756,8 +756,7 @@ static struct command_result *json_block_processed(struct command *cmd, send_account_balance_snapshot(wm->ld); } - /* TODO: channel_block_processed(wm->ld, *blockheight) lands in Group G - * when channel.c is migrated off chaintopology onto watchman. */ + channel_block_processed(wm->ld, *blockheight); notify_new_block(wm->ld); struct json_stream *response = json_stream_success(cmd); diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index f2bf28d7d71b..ea5168e486a8 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -762,14 +762,6 @@ u8 *unsigned_node_announcement(const tal_t *ctx UNNEEDED, struct lightningd *ld UNNEEDED, const u8 *prev UNNEEDED) { fprintf(stderr, "unsigned_node_announcement called!\n"); abort(); } -/* Generated stub for watch_blockdepth_ */ -bool watch_blockdepth_(const tal_t *ctx UNNEEDED, - struct chain_topology *topo UNNEEDED, - u32 blockheight UNNEEDED, - enum watch_result (*depthcb)(struct lightningd *ld UNNEEDED, u32 depth UNNEEDED, void *) UNNEEDED, - enum watch_result (*reorgcb)(struct lightningd *ld UNNEEDED, void *) UNNEEDED, - void *arg UNNEEDED) -{ fprintf(stderr, "watch_blockdepth_ called!\n"); abort(); } /* Generated stub for watch_opening_inflight */ void watch_opening_inflight(struct lightningd *ld UNNEEDED, struct channel_inflight *inflight UNNEEDED) @@ -817,6 +809,11 @@ void watchman_unwatch_scriptpubkey(struct lightningd *ld UNNEEDED, const u8 *scriptpubkey UNNEEDED, size_t script_len UNNEEDED) { fprintf(stderr, "watchman_unwatch_scriptpubkey called!\n"); abort(); } +/* Generated stub for watchman_watch_blockdepth */ +void watchman_watch_blockdepth(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + u32 confirm_height UNNEEDED) +{ fprintf(stderr, "watchman_watch_blockdepth called!\n"); abort(); } /* Generated stub for watchman_watch_outpoint */ void watchman_watch_outpoint(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, From 2edcd1089c15a8fce7321692baabd58d29458118 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 06:20:22 +0930 Subject: [PATCH 59/77] lightningd: add bwatch funding-spent + splice scaffolding Lays the inert scaffolding for moving funding-spend and splice detection off chaintopology and onto bwatch. No watches are created against the new owners yet, so the old watch_txo/funding_spent path is still live; the next commit flips the switch. The funding-spent and wrong-funding-spent handlers feed the existing onchaind_funding_spent, so callers don't need to know which watcher fired. wallet_insert_funding_spend stays at the call site for now because the old onchaind contract still expects it; it moves inside onchaind_funding_spent once onchaind itself runs on bwatch. The revert handler is a logging stub. A real rollback (kill onchaind, restore CHANNELD_NORMAL) needs onchaind to own its own bwatch watches first, so it lands with that migration. channel_splice_watch_found stashes the old outpoint in channel->pre_splice_funding before swapping channel->funding to the splice outpoint. bwatch updates channel->funding much earlier than chaintopology did, so handle_peer_splice_locked can no longer read the original outpoint off channel->funding when it does channel_record_splice; pre_splice_funding carries it across. channeld_tell_splice_depth keeps the cast_const for towire_channeld_funding_depth's mutable scid parameter contained in one place instead of leaking it into every caller. --- lightningd/channel.c | 2 + lightningd/channel.h | 6 ++ lightningd/channel_control.c | 33 ++++++- lightningd/channel_control.h | 7 ++ lightningd/peer_control.c | 168 +++++++++++++++++++++++++++++++++++ lightningd/peer_control.h | 35 ++++++++ lightningd/watchman.c | 5 ++ lightningd/watchman.h | 6 ++ 8 files changed, 260 insertions(+), 2 deletions(-) diff --git a/lightningd/channel.c b/lightningd/channel.c index 6ca82cb47681..dd40f94046ed 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -376,6 +376,7 @@ struct channel *new_unsaved_channel(struct peer *peer, channel->next_index[REMOTE] = 1; channel->next_htlc_id = 0; channel->funding_spend_watch = NULL; + channel->pre_splice_funding = NULL; /* FIXME: remove push when v1 deprecated */ channel->push = AMOUNT_MSAT(0); channel->closing_fee_negotiation_step = 50; @@ -614,6 +615,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid, channel->funding = *funding; channel->funding_sats = funding_sats; channel->funding_spend_watch = NULL; + channel->pre_splice_funding = NULL; channel->push = push; channel->our_funds = our_funds; channel->remote_channel_ready = remote_channel_ready; diff --git a/lightningd/channel.h b/lightningd/channel.h index b96666e04822..29b4bd8d6d4d 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -208,6 +208,12 @@ struct channel { /* Watch we have on funding output. */ struct txowatch *funding_spend_watch; + /* Original funding outpoint before a splice overwrites channel->funding. + * Populated by channel_splice_watch_found; read by handle_peer_splice_locked + * for channel_record_splice. In-memory only: not persisted to the wallet. + * NULL when no splice detection is pending. */ + struct bitcoin_outpoint *pre_splice_funding; + /* If we're doing a replay for onchaind, here are the txids it's watching */ struct replay_tx_hash *onchaind_replay_watches; /* Number of outstanding onchaind_spent calls */ diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index 9735e72a6699..a2fd7c5ca43b 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -1184,10 +1184,15 @@ static void handle_peer_splice_locked(struct channel *channel, const u8 *msg) &inflight->funding->outpoint); /* Stash prev funding data so we can log it after scid is updated - * (to get the blockheight) */ + * (to get the blockheight). After a splice, channel->funding holds + * the new outpoint, so use pre_splice_funding for the original. + * NULL means no splice — channel->funding is still the original. */ prev_our_msats = channel->our_msat; prev_funding_sats = channel->funding_sats; - prev_funding_out = channel->funding; + prev_funding_out = channel->pre_splice_funding + ? *channel->pre_splice_funding + : channel->funding; + channel->pre_splice_funding = tal_free(channel->pre_splice_funding); update_channel_from_inflight(channel->peer->ld, channel, inflight, true); @@ -2011,6 +2016,30 @@ void channeld_tell_depth(struct channel *channel, false, txid))); } +void channeld_tell_splice_depth(struct channel *channel, + const struct short_channel_id *splice_scid, + const struct bitcoin_txid *txid, + u32 depth) +{ + if (!channel->owner) { + log_debug(channel->log, + "Splice tx %s confirmed, but peer disconnected", + fmt_bitcoin_txid(tmpctx, txid)); + return; + } + + log_debug(channel->log, + "Sending splice funding_depth scid=%s depth=%u", + fmt_short_channel_id(tmpctx, *splice_scid), depth); + + /* towire_channeld_funding_depth takes a non-const scid. */ + subd_send_msg(channel->owner, + take(towire_channeld_funding_depth( + NULL, + cast_const(struct short_channel_id *, splice_scid), + depth, true, txid))); +} + /* Check if we are the fundee of this channel, the channel * funding transaction is still not yet seen onchain, and * it has been too long since the channel was first opened. diff --git a/lightningd/channel_control.h b/lightningd/channel_control.h index b5d78381641c..b25caf5f1d7c 100644 --- a/lightningd/channel_control.h +++ b/lightningd/channel_control.h @@ -22,6 +22,13 @@ void channeld_tell_depth(struct channel *channel, const struct bitcoin_txid *txid, u32 depth); +/* Send splice-specific funding_depth (is_splice=true) to channeld so it can + * begin splice lock-in. */ +void channeld_tell_splice_depth(struct channel *channel, + const struct short_channel_id *splice_scid, + const struct bitcoin_txid *txid, + u32 depth); + /* Notify channels of new blocks. */ void channel_notify_new_block(struct lightningd *ld); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 238cacb8c3e3..2f528c6c2197 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -2576,6 +2576,174 @@ static enum watch_result funding_spent(struct channel *channel, return onchaind_funding_spent(channel, tx, block->height); } +/* Splice tx confirmed: swap the outpoint watch from old to new funding and + * notify channeld. Returns true if the event was handled as a splice. */ +static UNUSED bool channel_splice_watch_found(struct lightningd *ld, + struct channel *channel, + const struct bitcoin_txid *txid, + size_t outnum, + const struct short_channel_id *scid, + u32 blockheight) +{ + struct channel_inflight *inflight; + + list_for_each(&channel->inflights, inflight, list) { + if (!bitcoin_txid_eq(txid, &inflight->funding->outpoint.txid) + || outnum != inflight->funding->outpoint.n) + continue; + + log_info(channel->log, + "bwatch: splice tx %s confirmed at block %u (scid %s)," + " switching outpoint watch", + fmt_bitcoin_txid(tmpctx, txid), + blockheight, + fmt_short_channel_id(tmpctx, *scid)); + + /* Stash the original outpoint before overwriting channel->funding. + * handle_peer_splice_locked reads this for channel_record_splice + * (needs the old outpoint, not the splice one). */ + channel->pre_splice_funding = tal_dup(channel, + struct bitcoin_outpoint, + &channel->funding); + + watchman_unwatch_outpoint(ld, + owner_channel_funding_spent(tmpctx, channel->dbid), + &channel->funding); + + channel->funding = inflight->funding->outpoint; + wallet_annotate_txout(ld->wallet, &channel->funding, + TX_CHANNEL_FUNDING, channel->dbid); + + /* Register the old scid as an alias so routing via the old scid + * keeps working immediately. channel_set_scid removes the old + * entry from the chanmap first, so channel_add_old_scid must + * come after it. */ + if (channel->scid) { + struct short_channel_id old_scid = *channel->scid; + channel_set_scid(channel, scid); + channel_add_old_scid(channel, old_scid); + } else { + channel_set_scid(channel, scid); + } + wallet_channel_save(ld->wallet, channel); + + watchman_watch_outpoint(ld, + owner_channel_funding_spent(tmpctx, channel->dbid), + &channel->funding, + blockheight); + + channeld_tell_splice_depth(channel, scid, txid, 1); + return true; + } + + return false; +} + +void channel_funding_spent_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t innum UNUSED, + u32 blockheight, + u32 txindex UNUSED) +{ + u64 dbid = strtoull(suffix, NULL, 10); + struct channel *channel = channel_by_dbid(ld, dbid); + struct bitcoin_txid spending_txid; + struct channel_inflight *inflight; + + if (!channel) { + log_broken(ld->log, + "channel/funding_spent watch_found: no channel for dbid %"PRIu64, + dbid); + return; + } + + bitcoin_txid(tx, &spending_txid); + log_info(channel->log, + "bwatch: funding outpoint %s:%u spent by %s at block %u", + fmt_bitcoin_txid(tmpctx, &channel->funding.txid), + channel->funding.n, + fmt_bitcoin_txid(tmpctx, &spending_txid), + blockheight); + + /* Splice in progress: the spending tx is one of our inflights, so the + * funding output is being legitimately consumed by our own splice. */ + list_for_each(&channel->inflights, inflight, list) { + if (bitcoin_txid_eq(&spending_txid, + &inflight->funding->outpoint.txid)) { + if (inflight->splice_locked_memonly) { + tal_free(inflight); + return; + } + return; + } + } + + wallet_insert_funding_spend(ld->wallet, channel, &spending_txid, 0, + blockheight); + onchaind_funding_spent(channel, tx, blockheight); +} + +void channel_funding_spent_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight) +{ + /* TODO: roll back onchaind state on funding-spend reorg. The full + * rollback (kill onchaind, restore CHANNELD_NORMAL, replay watches) + * needs onchaind itself to be running on bwatch first; until then, + * just record the event. */ + u64 dbid = strtoull(suffix, NULL, 10); + struct channel *channel = channel_by_dbid(ld, dbid); + + if (!channel) { + log_debug(ld->log, + "channel/funding_spent revert: unknown dbid %"PRIu64 + " at block %u, ignoring", + dbid, blockheight); + return; + } + + log_unusual(channel->log, + "channel/funding_spent revert at block %u (state %s):" + " no rollback yet", + blockheight, channel_state_name(channel)); +} + +void channel_wrong_funding_spent_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t innum UNUSED, + u32 blockheight, + u32 txindex UNUSED) +{ + u64 dbid = strtoull(suffix, NULL, 10); + struct channel *channel = channel_by_dbid(ld, dbid); + struct bitcoin_txid txid; + + if (!channel) { + log_broken(ld->log, + "channel/wrong_funding_spent watch_found:" + " no channel for dbid %"PRIu64, dbid); + return; + } + + bitcoin_txid(tx, &txid); + log_info(channel->log, + "bwatch: wrong funding outpoint spent by %s at block %u", + fmt_bitcoin_txid(tmpctx, &txid), blockheight); + + onchaind_funding_spent(channel, tx, blockheight); +} + +/* wrong_funding_spent and funding_spent both feed onchaind, so their + * reverts are the same. */ +void channel_wrong_funding_spent_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight) +{ + channel_funding_spent_watch_revert(ld, suffix, blockheight); +} + void channel_watch_wrong_funding(struct lightningd *ld, struct channel *channel) { /* Watch the "wrong" funding too, in case we spend it. */ diff --git a/lightningd/peer_control.h b/lightningd/peer_control.h index 4c68639e8ae7..a4e4a2b94257 100644 --- a/lightningd/peer_control.h +++ b/lightningd/peer_control.h @@ -186,6 +186,41 @@ void channel_watch_depth(struct lightningd *ld, /* If this channel has a "wrong funding" shutdown, watch that too. */ void channel_watch_wrong_funding(struct lightningd *ld, struct channel *channel); +/* bwatch handler for "channel/funding_spent/" (WATCH_OUTPOINT): the + * funding output was spent. If the spending tx is one of our own + * inflights, this is a splice in progress and we just keep watching + * (handing the memory-only inflight off to channel_splice_watch_found). + * Otherwise the channel was closed/force-closed, so hand off to onchaind. */ +void channel_funding_spent_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t innum, + u32 blockheight, + u32 txindex); + +/* Reorg of the funding-spend tx. Full rollback (kill onchaind, restore + * CHANNELD_NORMAL) lands once onchaind itself runs on bwatch; for now we + * just log the event. */ +void channel_funding_spent_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + +/* bwatch handler for "channel/wrong_funding_spent/": the + * shutdown_wrong_funding outpoint we registered in channel_watch_wrong_funding + * was spent. Handed off to onchaind the same way as channel_funding_spent. */ +void channel_wrong_funding_spent_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t innum, + u32 blockheight, + u32 txindex); + +/* Reorg of the wrong-funding-spend tx. Same handling as channel_funding_spent + * since both arrive at the same onchaind state machine. */ +void channel_wrong_funding_spent_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + /* How much can we spend in this channel? */ struct amount_msat channel_amount_spendable(const struct channel *channel); diff --git a/lightningd/watchman.c b/lightningd/watchman.c index be1f4b482146..b4df94183237 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -492,6 +492,11 @@ static const struct watch_dispatch { /* gossip/: WATCH_SCID, fires when the channel announcement UTXO is confirmed. * tx==NULL signals the SCID's expected position was absent from the block ("not found"). */ { "gossip/", gossip_scid_watch_found, gossip_scid_watch_revert }, + /* channel/funding_spent/: WATCH_OUTPOINT, fires when the funding outpoint is spent. + * Must precede "channel/funding/" so the longer prefix wins the strstarts() match. */ + { "channel/funding_spent/", channel_funding_spent_watch_found, channel_funding_spent_watch_revert }, + /* channel/wrong_funding_spent/: WATCH_OUTPOINT, fires when shutdown_wrong_funding outpoint is spent. */ + { "channel/wrong_funding_spent/", channel_wrong_funding_spent_watch_found, channel_wrong_funding_spent_watch_revert }, /* channel/funding/: WATCH_SCRIPTPUBKEY, fires when the funding output script * appears in a tx (i.e. the channel's funding transaction has been confirmed). */ { "channel/funding/", channel_funding_watch_found, channel_funding_watch_revert }, diff --git a/lightningd/watchman.h b/lightningd/watchman.h index cd23ce0e37a8..659c2bc96cb4 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -176,4 +176,10 @@ static inline const char *owner_channel_funding(const tal_t *ctx, u64 dbid) static inline const char *owner_channel_funding_depth(const tal_t *ctx, u64 dbid) { return tal_fmt(ctx, "channel/funding_depth/%"PRIu64, dbid); } +static inline const char *owner_channel_funding_spent(const tal_t *ctx, u64 dbid) +{ return tal_fmt(ctx, "channel/funding_spent/%"PRIu64, dbid); } + +static inline const char *owner_channel_wrong_funding_spent(const tal_t *ctx, u64 dbid) +{ return tal_fmt(ctx, "channel/wrong_funding_spent/%"PRIu64, dbid); } + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ From 910ac7b2af4267ad0f4e654dd362a3652347c349 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 07:46:58 +0930 Subject: [PATCH 60/77] lightningd: switch funding watches to bwatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shape of "watch the funding tx" depends on whether it's confirmed yet, and chaintopology was hiding that distinction behind two helpers that did similar things. bwatch makes the choice explicit: a scriptpubkey watch tells us when the tx lands, an outpoint watch tells us when it's spent, and you only ever want one or the other at a time. channel_watch_funding now branches on channel->scid for exactly that reason, and channel_watch_funding_out collapses into it because there's nothing left for it to do separately. channel_funding_watch_found gains a splice branch because, with bwatch, the same scriptpubkey watch sees both the original funding tx and any later splice — they share a P2WSH. Routing the not-our-expected-outpoint case into channel_splice_watch_found is the natural way to tell them apart. drop_to_chain's defensive funding_spend_watch fallback existed because chaintopology required us to register the spend watch late if a channel closed before fully opening. bwatch arms it up front in channel_watch_funding, so the fallback is just dead weight. The old static funding_spent cb is marked UNUSED rather than deleted; it and the inflight watch helpers are dead but still referenced from a few stubs and call sites that get cleaned up in the follow-up so this commit can stay about flipping the producer. --- lightningd/dual_open_control.c | 2 +- lightningd/peer_control.c | 159 ++++++++++++-------- lightningd/peer_control.h | 3 - lightningd/test/run-invoice-select-inchan.c | 36 +++-- wallet/test/run-wallet.c | 16 +- 5 files changed, 131 insertions(+), 85 deletions(-) diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index 3ea21b36c551..997043baa809 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -1967,7 +1967,7 @@ static void handle_channel_locked(struct subd *dualopend, /* That freed watchers in inflights: now watch funding tx */ channel_watch_depth(dualopend->ld, short_channel_id_blocknum(*channel->scid), channel); - channel_watch_funding_out(dualopend->ld, channel); + channel_watch_funding(dualopend->ld, channel); /* FIXME: LND sigs/update_fee msgs? */ peer_start_channeld(channel, peer_fd, NULL, false); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 2f528c6c2197..3591b3b43a58 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -325,10 +325,20 @@ static struct bitcoin_tx *sign_and_send_last(const tal_t *ctx, } /* FIXME: reorder! */ -static enum watch_result funding_spent(struct channel *channel, - const struct bitcoin_tx *tx, - size_t inputnum UNUSED, - const struct block *block); +/* UNUSED: chaintopology funding_spent path; bwatch + * channel_funding_spent_watch_found is the live producer. Removed in the + * follow-up cleanup commit. */ +static UNUSED enum watch_result funding_spent(struct channel *channel, + const struct bitcoin_tx *tx, + size_t inputnum UNUSED, + const struct block *block); + +static bool channel_splice_watch_found(struct lightningd *ld, + struct channel *channel, + const struct bitcoin_txid *txid, + size_t outnum, + const struct short_channel_id *scid, + u32 blockheight); /* We coop-closed channel: if another inflight confirms, force close */ static void closed_inflight_splice_found(struct lightningd *ld, @@ -400,16 +410,6 @@ void drop_to_chain(struct lightningd *ld, struct channel *channel, return; } - /* If we're not already (e.g. close before channel fully open), - * make sure we're watching for the funding spend */ - if (!channel->funding_spend_watch) { - log_debug(channel->log, "Adding funding_spend_watch"); - channel->funding_spend_watch = watch_txo(channel, - ld->topology, channel, - &channel->funding, - funding_spent); - } - /* If this was triggered by a close command, get a copy of the cmd id */ cmd_id = cmd_id_from_close_command(tmpctx, ld, channel); @@ -2321,13 +2321,15 @@ void channel_watch_depth(struct lightningd *ld, void channel_funding_watch_found(struct lightningd *ld, const char *suffix, - const struct bitcoin_tx *tx UNUSED, - size_t outnum UNUSED, + const struct bitcoin_tx *tx, + size_t outnum, u32 blockheight, u32 txindex) { u64 dbid = strtoull(suffix, NULL, 10); struct channel *channel = channel_by_dbid(ld, dbid); + struct bitcoin_txid txid; + struct short_channel_id scid; struct txlocator loc; if (!channel) { @@ -2337,6 +2339,31 @@ void channel_funding_watch_found(struct lightningd *ld, return; } + bitcoin_txid(tx, &txid); + + if (!mk_short_channel_id(&scid, blockheight, txindex, outnum)) { + log_broken(channel->log, + "bwatch: invalid scid from %u:%u:%zu", + blockheight, txindex, outnum); + return; + } + + /* Same funding scriptpubkey, different outpoint: this is a splice tx + * for an existing channel, not the original funding confirmation. */ + if (!bitcoin_txid_eq(&txid, &channel->funding.txid) + || outnum != channel->funding.n) { + if (channel_splice_watch_found(ld, channel, &txid, outnum, + &scid, blockheight)) + return; + log_unusual(channel->log, + "bwatch: funding watch_found for unexpected" + " outpoint %s:%zu (expected %s:%u), ignoring", + fmt_bitcoin_txid(tmpctx, &txid), outnum, + fmt_bitcoin_txid(tmpctx, &channel->funding.txid), + channel->funding.n); + return; + } + /* depthcb_update_scid() expects a txlocator; we have the block height * and txindex directly, so just fill one in on the stack. */ loc.blkheight = blockheight; @@ -2543,10 +2570,10 @@ void channel_block_processed(struct lightningd *ld, u32 blockheight) } } -static enum watch_result funding_spent(struct channel *channel, - const struct bitcoin_tx *tx, - size_t inputnum UNUSED, - const struct block *block) +static UNUSED enum watch_result funding_spent(struct channel *channel, + const struct bitcoin_tx *tx, + size_t inputnum UNUSED, + const struct block *block) { struct bitcoin_txid txid; struct channel_inflight *inflight; @@ -2578,12 +2605,12 @@ static enum watch_result funding_spent(struct channel *channel, /* Splice tx confirmed: swap the outpoint watch from old to new funding and * notify channeld. Returns true if the event was handled as a splice. */ -static UNUSED bool channel_splice_watch_found(struct lightningd *ld, - struct channel *channel, - const struct bitcoin_txid *txid, - size_t outnum, - const struct short_channel_id *scid, - u32 blockheight) +static bool channel_splice_watch_found(struct lightningd *ld, + struct channel *channel, + const struct bitcoin_txid *txid, + size_t outnum, + const struct short_channel_id *scid, + u32 blockheight) { struct channel_inflight *inflight; @@ -2746,68 +2773,78 @@ void channel_wrong_funding_spent_watch_revert(struct lightningd *ld, void channel_watch_wrong_funding(struct lightningd *ld, struct channel *channel) { - /* Watch the "wrong" funding too, in case we spend it. */ if (channel->shutdown_wrong_funding) { - watch_txo(channel, ld->topology, channel, - channel->shutdown_wrong_funding, - funding_spent); + watchman_watch_outpoint(ld, + owner_channel_wrong_funding_spent(tmpctx, channel->dbid), + channel->shutdown_wrong_funding, + get_block_height(ld->topology)); } } -void channel_watch_funding_out(struct lightningd *ld, struct channel *channel) -{ - tal_free(channel->funding_spend_watch); - channel->funding_spend_watch = watch_txo(channel, ld->topology, channel, - &channel->funding, - funding_spent); -} - void channel_watch_funding(struct lightningd *ld, struct channel *channel) { - log_debug(channel->log, "Watching for funding txid: %s", - fmt_bitcoin_txid(tmpctx, &channel->funding.txid)); - - /* This is stub channel, we don't watch anything funding. */ - if (!channel->scid || !is_stub_scid(*channel->scid)) { + if (!channel->scid) { + /* Funding tx not yet on-chain: register the scriptpubkey watch + * so bwatch tells us when (or if) it confirms. */ const u8 *funding_wscript = bitcoin_redeem_2of2(tmpctx, &channel->local_funding_pubkey, &channel->channel_info.remote_fundingkey); const u8 *funding_spk = scriptpubkey_p2wsh(tmpctx, funding_wscript); - /* Hand the funding scriptpubkey watch to bwatch. start_block - * is the current tip: rescan any past blocks if we're catching - * up after a restart, otherwise just watch from now. */ + log_debug(channel->log, + "bwatch: watching funding scriptpubkey for dbid %"PRIu64, + channel->dbid); watchman_watch_scriptpubkey(ld, - owner_channel_funding(tmpctx, - channel->dbid), + owner_channel_funding(tmpctx, channel->dbid), funding_spk, tal_bytelen(funding_spk), get_block_height(ld->topology)); + } else { + /* Funding confirmed (or stub with known outpoint): watch for spend. + * Stubs skip the scriptpubkey path because remote_fundingkey is a + * placeholder; the outpoint alone is enough. */ + log_debug(channel->log, + "bwatch: watching funding outpoint %s:%u for dbid %"PRIu64, + fmt_bitcoin_txid(tmpctx, &channel->funding.txid), + channel->funding.n, + channel->dbid); + watchman_watch_outpoint(ld, + owner_channel_funding_spent(tmpctx, channel->dbid), + &channel->funding, + get_block_height(ld->topology)); } - /* We watch for closing of course. */ - channel_watch_funding_out(ld, channel); channel_watch_wrong_funding(ld, channel); } void channel_unwatch_funding(struct lightningd *ld, struct channel *channel) { - const u8 *funding_wscript = bitcoin_redeem_2of2(tmpctx, - &channel->local_funding_pubkey, - &channel->channel_info.remote_fundingkey); + const u8 *funding_wscript; const u8 *funding_spk; - /* This is stub channel, we don't watch anything! */ + /* Stub channels have no watches. */ if (channel->scid && is_stub_scid(*channel->scid)) return; - funding_spk = scriptpubkey_p2wsh(tmpctx, funding_wscript); - watchman_unwatch_scriptpubkey(ld, - owner_channel_funding(tmpctx, - channel->dbid), - funding_spk, - tal_bytelen(funding_spk)); - /* FIXME: unwatch txo and depth too? */ + if (!channel->scid) { + /* Funding not yet on-chain: scriptpubkey watch is the active one. */ + funding_wscript = bitcoin_redeem_2of2(tmpctx, + &channel->local_funding_pubkey, + &channel->channel_info.remote_fundingkey); + funding_spk = scriptpubkey_p2wsh(tmpctx, funding_wscript); + watchman_unwatch_scriptpubkey(ld, + owner_channel_funding(tmpctx, channel->dbid), + funding_spk, + tal_bytelen(funding_spk)); + } else { + /* Funding confirmed: unwatch the spend outpoint and the depth tracker. */ + watchman_unwatch_outpoint(ld, + owner_channel_funding_spent(tmpctx, channel->dbid), + &channel->funding); + watchman_unwatch_blockdepth(ld, + owner_channel_funding_depth(tmpctx, channel->dbid), + short_channel_id_blocknum(*channel->scid)); + } } static void json_add_peer(struct lightningd *ld, diff --git a/lightningd/peer_control.h b/lightningd/peer_control.h index a4e4a2b94257..4dae41c909d8 100644 --- a/lightningd/peer_control.h +++ b/lightningd/peer_control.h @@ -175,9 +175,6 @@ void channel_funding_depth_revert(struct lightningd *ld, * depth-dependent state (lock-in, gossip announce, splice). */ void channel_block_processed(struct lightningd *ld, u32 blockheight); -/* Watch for spend of funding tx. */ -void channel_watch_funding_out(struct lightningd *ld, struct channel *channel); - /* Watch block that funding tx is in */ void channel_watch_depth(struct lightningd *ld, u32 blockheight, diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index d712672f755e..ff9d19152ab7 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -53,6 +53,10 @@ void broadcast_tx_(const tal_t *ctx UNNEEDED, bool (*refresh)(struct channel * UNNEEDED, const struct bitcoin_tx ** UNNEEDED, void *) UNNEEDED, void *cbarg TAKES UNNEEDED) { fprintf(stderr, "broadcast_tx_ called!\n"); abort(); } +/* Generated stub for channel_add_old_scid */ +void channel_add_old_scid(struct channel *channel UNNEEDED, + struct short_channel_id old_scid UNNEEDED) +{ fprintf(stderr, "channel_add_old_scid called!\n"); abort(); } /* Generated stub for channel_by_cid */ struct channel *channel_by_cid(struct lightningd *ld UNNEEDED, const struct channel_id *cid UNNEEDED) @@ -144,6 +148,12 @@ void channeld_tell_depth(struct channel *channel UNNEEDED, const struct bitcoin_txid *txid UNNEEDED, u32 depth UNNEEDED) { fprintf(stderr, "channeld_tell_depth called!\n"); abort(); } +/* Generated stub for channeld_tell_splice_depth */ +void channeld_tell_splice_depth(struct channel *channel UNNEEDED, + const struct short_channel_id *splice_scid UNNEEDED, + const struct bitcoin_txid *txid UNNEEDED, + u32 depth UNNEEDED) +{ fprintf(stderr, "channeld_tell_splice_depth called!\n"); abort(); } /* Generated stub for cmd_id_from_close_command */ const char *cmd_id_from_close_command(const tal_t *ctx UNNEEDED, struct lightningd *ld UNNEEDED, struct channel *channel UNNEEDED) @@ -661,6 +671,11 @@ u8 *towire_onchaind_dev_memleak(const tal_t *ctx UNNEEDED) /* Generated stub for towire_openingd_dev_memleak */ u8 *towire_openingd_dev_memleak(const tal_t *ctx UNNEEDED) { fprintf(stderr, "towire_openingd_dev_memleak called!\n"); abort(); } +/* Generated stub for wallet_annotate_txout */ +void wallet_annotate_txout(struct wallet *w UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED, + enum wallet_tx_type type UNNEEDED, u64 channel UNNEEDED) +{ fprintf(stderr, "wallet_annotate_txout called!\n"); abort(); } /* Generated stub for wallet_channel_save */ void wallet_channel_save(struct wallet *w UNNEEDED, struct channel *chan UNNEEDED) { fprintf(stderr, "wallet_channel_save called!\n"); abort(); } @@ -750,21 +765,16 @@ bool watch_scriptpubkey_(const tal_t *ctx UNNEEDED, void watch_splice_inflight(struct lightningd *ld UNNEEDED, struct channel_inflight *inflight UNNEEDED) { fprintf(stderr, "watch_splice_inflight called!\n"); abort(); } -/* Generated stub for watch_txo */ -struct txowatch *watch_txo(const tal_t *ctx UNNEEDED, - struct chain_topology *topo UNNEEDED, - struct channel *channel UNNEEDED, - const struct bitcoin_outpoint *outpoint UNNEEDED, - enum watch_result (*cb)(struct channel * UNNEEDED, - const struct bitcoin_tx *tx UNNEEDED, - size_t input_num UNNEEDED, - const struct block *block)) -{ fprintf(stderr, "watch_txo called!\n"); abort(); } /* Generated stub for watchman_unwatch_blockdepth */ void watchman_unwatch_blockdepth(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, u32 confirm_height UNNEEDED) { fprintf(stderr, "watchman_unwatch_blockdepth called!\n"); abort(); } +/* Generated stub for watchman_unwatch_outpoint */ +void watchman_unwatch_outpoint(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED) +{ fprintf(stderr, "watchman_unwatch_outpoint called!\n"); abort(); } /* Generated stub for watchman_unwatch_scriptpubkey */ void watchman_unwatch_scriptpubkey(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, @@ -776,6 +786,12 @@ void watchman_watch_blockdepth(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, u32 confirm_height UNNEEDED) { fprintf(stderr, "watchman_watch_blockdepth called!\n"); abort(); } +/* Generated stub for watchman_watch_outpoint */ +void watchman_watch_outpoint(struct lightningd *ld UNNEEDED, + const char *owner UNNEEDED, + const struct bitcoin_outpoint *outpoint UNNEEDED, + u32 start_block UNNEEDED) +{ fprintf(stderr, "watchman_watch_outpoint called!\n"); abort(); } /* Generated stub for watchman_watch_scriptpubkey */ void watchman_watch_scriptpubkey(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index ea5168e486a8..9f7a53aea5b6 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -131,6 +131,12 @@ void channeld_tell_depth(struct channel *channel UNNEEDED, const struct bitcoin_txid *txid UNNEEDED, u32 depth UNNEEDED) { fprintf(stderr, "channeld_tell_depth called!\n"); abort(); } +/* Generated stub for channeld_tell_splice_depth */ +void channeld_tell_splice_depth(struct channel *channel UNNEEDED, + const struct short_channel_id *splice_scid UNNEEDED, + const struct bitcoin_txid *txid UNNEEDED, + u32 depth UNNEEDED) +{ fprintf(stderr, "channeld_tell_splice_depth called!\n"); abort(); } /* Generated stub for check_announce_sigs */ const char *check_announce_sigs(const struct channel *channel UNNEEDED, struct short_channel_id scid UNNEEDED, @@ -783,16 +789,6 @@ bool watch_scriptpubkey_(const tal_t *ctx UNNEEDED, void watch_splice_inflight(struct lightningd *ld UNNEEDED, struct channel_inflight *inflight UNNEEDED) { fprintf(stderr, "watch_splice_inflight called!\n"); abort(); } -/* Generated stub for watch_txo */ -struct txowatch *watch_txo(const tal_t *ctx UNNEEDED, - struct chain_topology *topo UNNEEDED, - struct channel *channel UNNEEDED, - const struct bitcoin_outpoint *outpoint UNNEEDED, - enum watch_result (*cb)(struct channel * UNNEEDED, - const struct bitcoin_tx *tx UNNEEDED, - size_t input_num UNNEEDED, - const struct block *block)) -{ fprintf(stderr, "watch_txo called!\n"); abort(); } /* Generated stub for watchman_unwatch_blockdepth */ void watchman_unwatch_blockdepth(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, From dbd924ff427bb9840d200c77effe7a8becdf1673 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 08:39:22 +0930 Subject: [PATCH 61/77] lightningd: drop chaintopology funding callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With bwatch driving funding watches (previous commit), the per-channel chaintopology callbacks are dead weight: they exist to solve "tick me every block once my tx confirms", and bwatch already gives us that collectively via channel_block_processed. Spreading the same per-block work across N independent watches with N callbacks is just bookkeeping overhead now. So opening_depth_cb / opening_reorged_cb / dual_funding_found in dualopend, splice_depth_cb / splice_reorged_cb / splice_found in channeld, and the static funding_spent in peer_control all collapse into one place: the existing per-channel loop in channel_block_processed. Each channel state that wants depth ticks gets its own case, including DUALOPEND_AWAITING_LOCKIN routed to a tiny new dualopend_channel_depth helper that does what opening_depth_cb used to do (push depth, promote the matching inflight at minimum_depth). CHANNELD_AWAITING_SPLICE switches from channeld_tell_depth to channeld_tell_splice_depth. The old splice_depth_cb path was sending the splice-flagged depth message; collapsing into channel_block_processed without that switch would silently change wire behaviour and break splice lock-in. watch_opening_inflight and watch_splice_inflight stay as one-line wrappers around channel_watch_funding so existing callers don't need to know which watcher actually carries the inflight. setup_peer no longer iterates inflights for AWAITING_LOCKIN / AWAITING_SPLICE — channel_watch_funding registers a single scriptpubkey watch keyed by dbid, and that one watch covers every inflight (they all share the funding P2WSH). --- lightningd/channel_control.c | 84 +-------------------- lightningd/dual_open_control.c | 71 ++++++----------- lightningd/dual_open_control.h | 6 ++ lightningd/peer_control.c | 83 +++++++------------- lightningd/test/run-invoice-select-inchan.c | 13 ++-- wallet/test/run-wallet.c | 13 ++-- 6 files changed, 73 insertions(+), 197 deletions(-) diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index a2fd7c5ca43b..d3934e211a2b 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -685,90 +685,12 @@ static void handle_splice_confirmed_signed(struct lightningd *ld, send_splice_tx(channel, tx, cc, output_index, inflight->funding_psbt); } -static enum watch_result splice_depth_cb(struct lightningd *ld, - unsigned int depth, - struct channel_inflight *inflight) -{ - /* Usually, we're here because we're awaiting a splice, but - * we could also mutual shutdown, or that weird splice_locked_memonly - * hack... */ - if (inflight->channel->state != CHANNELD_AWAITING_SPLICE) { - log_debug(inflight->channel->log, "Splice inflight event but not" - " in AWAITING_SPLICE, ending watch of txid %s", - fmt_bitcoin_txid(tmpctx, &inflight->funding->outpoint.txid)); - return DELETE_WATCH; - } - - if (inflight->channel->owner) { - log_debug(inflight->channel->log, "splice_depth_cb: sending funding depth scid: %s", - fmt_short_channel_id(tmpctx, *inflight->scid)); - subd_send_msg(inflight->channel->owner, - take(towire_channeld_funding_depth( - NULL, inflight->scid, - depth, true, - &inflight->funding->outpoint.txid))); - } - - /* channeld will tell us when splice is locked in: we'll clean - * this watch up then. */ - return KEEP_WATCHING; -} - -/* Reorged out? OK, we're not committed yet. */ -static enum watch_result splice_reorged_cb(struct lightningd *ld, struct channel_inflight *inflight) -{ - log_unusual(inflight->channel->log, "Splice inflight txid %s reorged out", - fmt_bitcoin_txid(tmpctx, &inflight->funding->outpoint.txid)); - inflight->scid = tal_free(inflight->scid); - return DELETE_WATCH; -} - -/* We see this tx output spend to the splice funding address. */ -static void splice_found(struct lightningd *ld, - const struct bitcoin_tx *tx, - u32 outnum, - const struct txlocator *loc, - struct channel_inflight *inflight) -{ - assert(!inflight->scid); - inflight->scid = tal(inflight, struct short_channel_id); - - if (!mk_short_channel_id(inflight->scid, - loc->blkheight, loc->index, - inflight->funding->outpoint.n)) { - inflight->scid = tal_free(inflight->scid); - channel_fail_permanent(inflight->channel, - REASON_LOCAL, - "Invalid funding scid %u:%u:%u", - loc->blkheight, loc->index, - inflight->funding->outpoint.n); - return; - } - - /* We will almost immediately get called, which is what we want! */ - watch_blockdepth(inflight, ld->topology, loc->blkheight, - splice_depth_cb, - splice_reorged_cb, - inflight); -} - +/* Thin wrapper kept so callers don't need to know the bwatch script watch + * (same P2WSH as the original funding) is what actually picks up the splice. */ void watch_splice_inflight(struct lightningd *ld, struct channel_inflight *inflight) { - const u8 *funding_wscript = bitcoin_redeem_2of2(tmpctx, - &inflight->channel->local_funding_pubkey, - inflight->funding->splice_remote_funding); - - log_info(inflight->channel->log, "Watching splice inflight %s", - fmt_bitcoin_txid(tmpctx, - &inflight->funding->outpoint.txid)); - - watch_scriptpubkey(inflight, ld->topology, - take(scriptpubkey_p2wsh(NULL, funding_wscript)), - &inflight->funding->outpoint, - inflight->funding->total_funds, - splice_found, - inflight); + channel_watch_funding(ld, inflight->channel); } static void handle_splice_sending_sigs(struct lightningd *ld, diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index 997043baa809..a3811abdd71b 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -1002,61 +1003,39 @@ static void dualopend_tell_depth(struct channel *channel, to_go)); } -static enum watch_result opening_depth_cb(struct lightningd *ld, - unsigned int depth, - struct channel_inflight *inflight) -{ - /* Usually, we're here because we're awaiting a lockin, but - * we could also mutual shutdown */ - if (inflight->channel->state != DUALOPEND_AWAITING_LOCKIN) - return DELETE_WATCH; - - if (depth >= inflight->channel->minimum_depth) - update_channel_from_inflight(ld, inflight->channel, inflight, - false); - - dualopend_tell_depth(inflight->channel, &inflight->funding->outpoint.txid, depth); - return KEEP_WATCHING; -} - -static enum watch_result opening_reorged_cb(struct lightningd *ld, struct channel_inflight *inflight) +void dualopend_channel_depth(struct lightningd *ld, + struct channel *channel, + u32 depth) { - /* Reorged out? OK, we're not committed yet. */ - log_info(inflight->channel->log, "Candidate funding tx was in a block, now reorged out"); - return DELETE_WATCH; -} + struct channel_inflight *inflight; -static void dual_funding_found(struct lightningd *ld, - const struct bitcoin_tx *tx, - u32 outnum, - const struct txlocator *loc, - struct channel_inflight *inflight) -{ - /* Kill it if the channel funding isn't a valid scid */ - if (!depthcb_update_scid(inflight->channel, - &inflight->funding->outpoint, - loc)) + /* No scid yet means funding hasn't confirmed; nothing to report. */ + if (!channel->scid) return; - /* Otherwise, watch for block depth increases (we'll immediately expect one) */ - watch_blockdepth(inflight, ld->topology, loc->blkheight, - opening_depth_cb, - opening_reorged_cb, - inflight); + /* Push the new depth to dualopend so it can update its billboard + * and decide when to send funding_locked. */ + dualopend_tell_depth(channel, &channel->funding.txid, depth); + + /* At/past minimum depth: promote the matching inflight into the + * channel's funding fields (funding_sats, our_msat, etc.) so the + * rest of the daemon sees the live numbers from here on. */ + if (depth >= channel->minimum_depth) { + list_for_each(&channel->inflights, inflight, list) { + if (bitcoin_outpoint_eq(&inflight->funding->outpoint, + &channel->funding)) { + update_channel_from_inflight(ld, channel, + inflight, false); + break; + } + } + } } void watch_opening_inflight(struct lightningd *ld, struct channel_inflight *inflight) { - const u8 *funding_wscript = bitcoin_redeem_2of2(tmpctx, - &inflight->channel->local_funding_pubkey, - &inflight->channel->channel_info.remote_fundingkey); - watch_scriptpubkey(inflight, ld->topology, - take(scriptpubkey_p2wsh(NULL, funding_wscript)), - &inflight->funding->outpoint, - inflight->funding->total_funds, - dual_funding_found, - inflight); + channel_watch_funding(ld, inflight->channel); } static void diff --git a/lightningd/dual_open_control.h b/lightningd/dual_open_control.h index c9d4c4d72952..65eba35eb0ff 100644 --- a/lightningd/dual_open_control.h +++ b/lightningd/dual_open_control.h @@ -17,6 +17,12 @@ bool peer_restart_dualopend(struct peer *peer, void watch_opening_inflight(struct lightningd *ld, struct channel_inflight *inflight); +/* Per-block depth driver for DUALOPEND_AWAITING_LOCKIN channels, called + * from channel_block_processed. */ +void dualopend_channel_depth(struct lightningd *ld, + struct channel *channel, + u32 depth); + /* Close connection to an unsaved channel */ void channel_unsaved_close_conn(struct channel *channel, const char *why); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 3591b3b43a58..ffb83828b250 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -324,15 +324,6 @@ static struct bitcoin_tx *sign_and_send_last(const tal_t *ctx, return tx; } -/* FIXME: reorder! */ -/* UNUSED: chaintopology funding_spent path; bwatch - * channel_funding_spent_watch_found is the live producer. Removed in the - * follow-up cleanup commit. */ -static UNUSED enum watch_result funding_spent(struct channel *channel, - const struct bitcoin_tx *tx, - size_t inputnum UNUSED, - const struct block *block); - static bool channel_splice_watch_found(struct lightningd *ld, struct channel *channel, const struct bitcoin_txid *txid, @@ -2555,52 +2546,41 @@ void channel_block_processed(struct lightningd *ld, u32 blockheight) lockin_complete(channel, CHANNELD_AWAITING_LOCKIN); break; + case DUALOPEND_AWAITING_LOCKIN: + dualopend_channel_depth(ld, channel, depth); + break; case CHANNELD_NORMAL: - case CHANNELD_AWAITING_SPLICE: channeld_tell_depth(channel, &channel->funding.txid, depth); break; - default: - /* DUALOPEND_*, AWAITING_UNILATERAL, CLOSED, etc. - * keep using their existing depth-driving paths. */ + case CHANNELD_AWAITING_SPLICE: + /* channel->scid and channel->funding.txid both + * track the live funding (original before the + * splice tx confirms; splice after). Either + * way, depth was computed from channel->scid + * above, so this call is correct in both. */ + channeld_tell_splice_depth(channel, + channel->scid, + &channel->funding.txid, + depth); break; - } - } - } -} -static UNUSED enum watch_result funding_spent(struct channel *channel, - const struct bitcoin_tx *tx, - size_t inputnum UNUSED, - const struct block *block) -{ - struct bitcoin_txid txid; - struct channel_inflight *inflight; - - bitcoin_txid(tx, &txid); - - /* If we're doing a splice, we expect the funding transaction to be - * spent, so don't freak out and just keep watching in that case */ - list_for_each(&channel->inflights, inflight, list) { - if (bitcoin_txid_eq(&txid, - &inflight->funding->outpoint.txid)) { - /* splice_locked is a special flag that indicates this - * is a memory-only inflight acting as a race condition - * safeguard. When we see this, it is our responsability - * to clean up this memory-only inflight. */ - if (inflight->splice_locked_memonly) { - tal_free(inflight); - return DELETE_WATCH; + /* No channeld to notify. */ + case DUALOPEND_OPEN_INIT: + case DUALOPEND_OPEN_COMMIT_READY: + case DUALOPEND_OPEN_COMMITTED: + case CHANNELD_SHUTTING_DOWN: + case CLOSINGD_SIGEXCHANGE: + case CLOSINGD_COMPLETE: + case AWAITING_UNILATERAL: + case FUNDING_SPEND_SEEN: + case ONCHAIN: + case CLOSED: + break; } - return KEEP_WATCHING; } } - - wallet_insert_funding_spend(channel->peer->ld->wallet, channel, - &txid, 0, block->height); - - return onchaind_funding_spent(channel, tx, block->height); } /* Splice tx confirmed: swap the outpoint watch from old to new funding and @@ -3119,7 +3099,6 @@ command_find_channel(struct command *cmd, static void setup_peer(struct peer *peer) { struct channel *channel; - struct channel_inflight *inflight; struct lightningd *ld = peer->ld; bool connect = false, important = false; @@ -3145,16 +3124,12 @@ static void setup_peer(struct peer *peer) channel_watch_funding(ld, channel); break; - /* We need to watch all inflights which may open channel */ + /* The single bwatch scriptpubkey watch covers every inflight + * (they all share the funding P2WSH); depth is driven by + * channel_block_processed. */ case DUALOPEND_AWAITING_LOCKIN: - list_for_each(&channel->inflights, inflight, list) - watch_opening_inflight(ld, inflight); - break; - - /* We need to watch all inflights which may splice */ case CHANNELD_AWAITING_SPLICE: - list_for_each(&channel->inflights, inflight, list) - watch_splice_inflight(ld, inflight); + channel_watch_funding(ld, channel); break; } diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index ff9d19152ab7..18596dee0a25 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -265,6 +265,11 @@ bool depthcb_update_scid(struct channel *channel UNNEEDED, /* Generated stub for dev_disconnect_permanent */ bool dev_disconnect_permanent(struct lightningd *ld UNNEEDED) { fprintf(stderr, "dev_disconnect_permanent called!\n"); abort(); } +/* Generated stub for dualopend_channel_depth */ +void dualopend_channel_depth(struct lightningd *ld UNNEEDED, + struct channel *channel UNNEEDED, + u32 depth UNNEEDED) +{ fprintf(stderr, "dualopend_channel_depth called!\n"); abort(); } /* Generated stub for fatal */ void fatal(const char *fmt UNNEEDED, ...) { fprintf(stderr, "fatal called!\n"); abort(); } @@ -744,10 +749,6 @@ void wallet_unreserve_utxo(struct wallet *w UNNEEDED, struct utxo *utxo UNNEEDED struct utxo *wallet_utxo_get(const tal_t *ctx UNNEEDED, struct wallet *w UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED) { fprintf(stderr, "wallet_utxo_get called!\n"); abort(); } -/* Generated stub for watch_opening_inflight */ -void watch_opening_inflight(struct lightningd *ld UNNEEDED, - struct channel_inflight *inflight UNNEEDED) -{ fprintf(stderr, "watch_opening_inflight called!\n"); abort(); } /* Generated stub for watch_scriptpubkey_ */ bool watch_scriptpubkey_(const tal_t *ctx UNNEEDED, struct chain_topology *topo UNNEEDED, @@ -761,10 +762,6 @@ bool watch_scriptpubkey_(const tal_t *ctx UNNEEDED, void *) UNNEEDED, void *arg UNNEEDED) { fprintf(stderr, "watch_scriptpubkey_ called!\n"); abort(); } -/* Generated stub for watch_splice_inflight */ -void watch_splice_inflight(struct lightningd *ld UNNEEDED, - struct channel_inflight *inflight UNNEEDED) -{ fprintf(stderr, "watch_splice_inflight called!\n"); abort(); } /* Generated stub for watchman_unwatch_blockdepth */ void watchman_unwatch_blockdepth(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 9f7a53aea5b6..1f5aa62c5d57 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -256,6 +256,11 @@ bool depthcb_update_scid(struct channel *channel UNNEEDED, /* Generated stub for dev_disconnect_permanent */ bool dev_disconnect_permanent(struct lightningd *ld UNNEEDED) { fprintf(stderr, "dev_disconnect_permanent called!\n"); abort(); } +/* Generated stub for dualopend_channel_depth */ +void dualopend_channel_depth(struct lightningd *ld UNNEEDED, + struct channel *channel UNNEEDED, + u32 depth UNNEEDED) +{ fprintf(stderr, "dualopend_channel_depth called!\n"); abort(); } /* Generated stub for fatal */ void fatal(const char *fmt UNNEEDED, ...) { fprintf(stderr, "fatal called!\n"); abort(); } @@ -768,10 +773,6 @@ u8 *unsigned_node_announcement(const tal_t *ctx UNNEEDED, struct lightningd *ld UNNEEDED, const u8 *prev UNNEEDED) { fprintf(stderr, "unsigned_node_announcement called!\n"); abort(); } -/* Generated stub for watch_opening_inflight */ -void watch_opening_inflight(struct lightningd *ld UNNEEDED, - struct channel_inflight *inflight UNNEEDED) -{ fprintf(stderr, "watch_opening_inflight called!\n"); abort(); } /* Generated stub for watch_scriptpubkey_ */ bool watch_scriptpubkey_(const tal_t *ctx UNNEEDED, struct chain_topology *topo UNNEEDED, @@ -785,10 +786,6 @@ bool watch_scriptpubkey_(const tal_t *ctx UNNEEDED, void *) UNNEEDED, void *arg UNNEEDED) { fprintf(stderr, "watch_scriptpubkey_ called!\n"); abort(); } -/* Generated stub for watch_splice_inflight */ -void watch_splice_inflight(struct lightningd *ld UNNEEDED, - struct channel_inflight *inflight UNNEEDED) -{ fprintf(stderr, "watch_splice_inflight called!\n"); abort(); } /* Generated stub for watchman_unwatch_blockdepth */ void watchman_unwatch_blockdepth(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, From 0a0aa69895ba9f29e6d0c78b3ba0d573cc4b146c Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 09:15:57 +0930 Subject: [PATCH 62/77] lightningd: scaffold onchaind bwatch tracking Sets up the data structures and helpers that later commits will use to move onchaind off chaintopology. Nothing references this scaffolding yet, so the existing replay path is still in charge. Onchaind asks us to watch specific spending txs and only the outputs of those txs it cares about, so we key everything by (channel dbid, txid) rather than per-channel. funding_spend_txid is kept in memory only -- it's repopulated on restart by the channel_close depth watch before onchaind runs, which is also what lets us re-spawn onchaind without persisting a separate "is this channel closing?" bit. --- lightningd/channel.c | 4 ++ lightningd/channel.h | 14 +++++ lightningd/onchain_control.c | 101 +++++++++++++++++++++++++++++++++++ lightningd/onchain_control.h | 5 ++ lightningd/watchman.c | 1 - lightningd/watchman.h | 25 +++++++++ 6 files changed, 149 insertions(+), 1 deletion(-) diff --git a/lightningd/channel.c b/lightningd/channel.c index dd40f94046ed..faa62bdd767e 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -377,6 +377,8 @@ struct channel *new_unsaved_channel(struct peer *peer, channel->next_htlc_id = 0; channel->funding_spend_watch = NULL; channel->pre_splice_funding = NULL; + channel->onchaind_watches = NULL; + channel->funding_spend_txid = NULL; /* FIXME: remove push when v1 deprecated */ channel->push = AMOUNT_MSAT(0); channel->closing_fee_negotiation_step = 50; @@ -616,6 +618,8 @@ struct channel *new_channel(struct peer *peer, u64 dbid, channel->funding_sats = funding_sats; channel->funding_spend_watch = NULL; channel->pre_splice_funding = NULL; + channel->onchaind_watches = NULL; + channel->funding_spend_txid = NULL; channel->push = push; channel->our_funds = our_funds; channel->remote_channel_ready = remote_channel_ready; diff --git a/lightningd/channel.h b/lightningd/channel.h index 29b4bd8d6d4d..65cd4944d9ed 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -14,6 +14,7 @@ #include #include +struct onchaind_tx_map; struct uncommitted_channel; struct wally_psbt; @@ -221,6 +222,19 @@ struct channel { /* Height we're replaying at (if onchaind_replay_watches set) */ u32 onchaind_replay_height; + /* Per-session map of txs onchaind has asked us to watch: + * txid -> {confirm height, the outpoints we registered}. + * Initialised by onchaind_funding_spent; NULL before onchaind starts. + * onchaind_clear_watches walks it to tear everything down on reorg. */ + struct onchaind_tx_map *onchaind_watches; + + /* The txid of the tx that spent our funding output, set by + * onchaind_funding_spent. Used by channel_funding_spent_watch_revert + * to know we actually saw a spend (and to build the channel_close + * blockdepth owner string for unwatch). In-memory only: repopulated + * on restart by the channel_close depth handler before onchaind runs. */ + struct bitcoin_txid *funding_spend_txid; + /* Our original funds, in funding amount */ struct amount_sat our_funds; diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c index 2a0fa58307e5..b5e26662a3a5 100644 --- a/lightningd/onchain_control.c +++ b/lightningd/onchain_control.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -43,6 +44,106 @@ static bool replay_tx_eq_txid(const struct replay_tx *rtx, HTABLE_DEFINE_NODUPS_TYPE(struct replay_tx, replay_tx_keyof, txid_hash, replay_tx_eq_txid, replay_tx_hash); +/* Per-channel record of one tx onchaind is tracking. + * - blockheight: confirmation height; needed to unwatch the matching + * blockdepth watch on reorg or full resolution. + * - txid: the spending tx whose outputs we're watching. + * - outpoints: only the outputs onchaind asked about (typically HTLC and + * to_us outputs of a unilateral close), not all outputs of the tx. */ +struct onchaind_watched_tx { + u32 blockheight; + struct bitcoin_txid txid; + struct bitcoin_outpoint *outpoints; +}; + +static const struct bitcoin_txid * +onchaind_watched_tx_keyof(const struct onchaind_watched_tx *entry) +{ + return &entry->txid; +} + +static bool onchaind_watched_tx_eq_txid(const struct onchaind_watched_tx *entry, + const struct bitcoin_txid *txid) +{ + return bitcoin_txid_eq(&entry->txid, txid); +} + +static size_t onchaind_txid_hash(const struct bitcoin_txid *txid) +{ + size_t ret; + memcpy(&ret, txid, sizeof(ret)); + return ret; +} + +HTABLE_DEFINE_NODUPS_TYPE(struct onchaind_watched_tx, + onchaind_watched_tx_keyof, + onchaind_txid_hash, + onchaind_watched_tx_eq_txid, + onchaind_tx_map); + +/* Silence -Wunused-function for the generated _add until the producer side + * (onchaind_funding_spent / bwatch_watch_outpoints) is wired up later. */ +static void UNUSED onchaind_tx_map_silence_unused(struct onchaind_tx_map *m, + struct onchaind_watched_tx *e) +{ + onchaind_tx_map_add(m, e); +} + +/* Drop all bwatch watches we registered for this entry: the per-tx blockdepth + * watch and every outpoint watch under the same owner. Idempotent on the + * outpoint side because bwatch tolerates duplicate deletes. */ +static void UNUSED unwatch_entry(struct channel *channel, + struct onchaind_watched_tx *entry) +{ + struct lightningd *ld = channel->peer->ld; + const char *owner_out = owner_onchaind_outpoint(tmpctx, channel->dbid, + &entry->txid); + + for (size_t i = 0; i < tal_count(entry->outpoints); i++) + watchman_unwatch_outpoint(ld, owner_out, &entry->outpoints[i]); + + watchman_unwatch_blockdepth(ld, + owner_onchaind_depth(tmpctx, channel->dbid, + &entry->txid), + entry->blockheight); +} + +void onchaind_clear_watches(struct channel *channel) +{ + struct onchaind_tx_map_iter it; + struct onchaind_watched_tx *entry; + struct lightningd *ld = channel->peer->ld; + + /* Restart marker first: this is the depth watch on the funding-spend + * tx itself that lets us re-spawn onchaind across restarts. Once it's + * gone we are committed to not coming back to this close. */ + if (channel->funding_spend_txid) { + u32 spend_blockheight = channel->close_blockheight + ? *channel->close_blockheight + : wallet_transaction_height(ld->wallet, + channel->funding_spend_txid); + if (spend_blockheight) + watchman_unwatch_blockdepth( + ld, + owner_onchaind_channel_close( + tmpctx, channel->dbid, + channel->funding_spend_txid), + spend_blockheight); + channel->funding_spend_txid + = tal_free(channel->funding_spend_txid); + } + + if (!channel->onchaind_watches) + return; + + for (entry = onchaind_tx_map_first(channel->onchaind_watches, &it); + entry; + entry = onchaind_tx_map_next(channel->onchaind_watches, &it)) + unwatch_entry(channel, entry); + + channel->onchaind_watches = tal_free(channel->onchaind_watches); +} + /* We dump all the known preimages when onchaind starts up. */ static void onchaind_tell_fulfill(struct channel *channel) { diff --git a/lightningd/onchain_control.h b/lightningd/onchain_control.h index c0fe3d3b09dd..2da32139479a 100644 --- a/lightningd/onchain_control.h +++ b/lightningd/onchain_control.h @@ -13,4 +13,9 @@ enum watch_result onchaind_funding_spent(struct channel *channel, void onchaind_replay_channels(struct lightningd *ld); +/* Tear down all bwatch watches that onchaind registered for this channel. + * Called when the funding-spend tx is reorged out (channel is no longer + * closing) or when we lose track of an onchaind session for any reason. */ +void onchaind_clear_watches(struct channel *channel); + #endif /* LIGHTNING_LIGHTNINGD_ONCHAIN_CONTROL_H */ diff --git a/lightningd/watchman.c b/lightningd/watchman.c index b4df94183237..7258e9f5eccd 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include diff --git a/lightningd/watchman.h b/lightningd/watchman.h index 659c2bc96cb4..e1c77d64f525 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -6,6 +6,7 @@ #include #include #include +#include #include struct lightningd; @@ -182,4 +183,28 @@ static inline const char *owner_channel_funding_spent(const tal_t *ctx, u64 dbid static inline const char *owner_channel_wrong_funding_spent(const tal_t *ctx, u64 dbid) { return tal_fmt(ctx, "channel/wrong_funding_spent/%"PRIu64, dbid); } +/* onchaind/ owners. + * + * Per-tx (not per-channel): onchaind asks us to track a particular spending tx + * and the specific outpoints of that tx it cares about, so the dbid+txid pair + * is the unit of identity here. Txid is rendered in internal byte order via + * tal_hexstr so revert handlers can hex_decode the suffix back into a txid. */ +static inline const char *owner_onchaind_outpoint(const tal_t *ctx, u64 dbid, + const struct bitcoin_txid *txid) +{ return tal_fmt(ctx, "onchaind/outpoint/%"PRIu64"/%s", + dbid, tal_hexstr(ctx, txid, sizeof(*txid))); } + +static inline const char *owner_onchaind_depth(const tal_t *ctx, u64 dbid, + const struct bitcoin_txid *txid) +{ return tal_fmt(ctx, "onchaind/depth/%"PRIu64"/%s", + dbid, tal_hexstr(ctx, txid, sizeof(*txid))); } + +/* Restart marker: depth watch on the funding-spend tx itself, used to + * re-spawn onchaind across lightningd restarts. Uses ':' to separate dbid + * from txid, consistent with wallet/utxo/:. */ +static inline const char *owner_onchaind_channel_close(const tal_t *ctx, u64 dbid, + const struct bitcoin_txid *txid) +{ return tal_fmt(ctx, "onchaind/channel_close/%"PRIu64":%s", + dbid, tal_hexstr(ctx, txid, sizeof(*txid))); } + #endif /* LIGHTNING_LIGHTNINGD_WATCHMAN_H */ From f8b6f56972ef93bcf7f2faa6f22cf7fbbe94678d Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 09:27:06 +0930 Subject: [PATCH 63/77] lightningd: roll back onchaind on funding-spend reorg The hard part of this revert isn't tearing watches down -- it's deciding when not to. bwatch fires this revert whenever the funding-confirmation block is reorged, regardless of whether the outpoint was actually spent. Use funding_spend_txid as a "we actually saw a spend" signal so a deep funding reorg doesn't look like a closing-tx reorg. If we're already ONCHAIN, ignore: in practice this is a bwatch startup resync (watchman height < bwatch tip), not a real deep reorg, and there's no sane way to undo a fully-handed-off channel anyway. Rollback order: kill onchaind first, drop bwatch watches before clearing close_blockheight (the unwatch needs that height), and reset gossip before persisting via a new channel_gossip_funding_reorg helper -- the gossip state machine has no legal backward transition from ONCHAIN. --- lightningd/channel_gossip.c | 19 ++++++++++++ lightningd/channel_gossip.h | 3 ++ lightningd/peer_control.c | 58 ++++++++++++++++++++++++++++++------- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/lightningd/channel_gossip.c b/lightningd/channel_gossip.c index 5daa79b68080..e96e7577d307 100644 --- a/lightningd/channel_gossip.c +++ b/lightningd/channel_gossip.c @@ -950,6 +950,25 @@ void channel_gossip_init(struct channel *channel, check_channel_gossip(channel); } +void channel_gossip_funding_reorg(struct channel *channel) +{ + struct channel_gossip *cg = channel->channel_gossip; + if (!cg) + return; + + /* Stashed remote sigs reference the old scid; drop them so the + * fresh announcement (if any) doesn't try to use them. */ + cg->remote_sigs = tal_free(cg->remote_sigs); + + /* The state machine has no legal backward transition, so re-derive + * from the channel's current properties instead of trying to walk + * back through it. */ + cg->state = derive_channel_state(channel); + log_debug(channel->log, + "channel_gossip: reset to %s after funding reorg", + channel_gossip_state_str(cg->state)); +} + /* Something about channel changed: update if required */ void channel_gossip_update(struct channel *channel) { diff --git a/lightningd/channel_gossip.h b/lightningd/channel_gossip.h index b6fae02a204e..6a7fbd0f9ca2 100644 --- a/lightningd/channel_gossip.h +++ b/lightningd/channel_gossip.h @@ -19,6 +19,9 @@ void channel_gossip_startup_done(struct lightningd *ld); /* Something about channel/blockchain changed: update if required */ void channel_gossip_update(struct channel *channel); +/* Funding tx was reorged out: reset gossip state to match new reality. */ +void channel_gossip_funding_reorg(struct channel *channel); + /* Short channel id changed (splice, or reorg). */ void channel_gossip_scid_changed(struct channel *channel); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index ffb83828b250..cf0e21984b08 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -2695,25 +2695,63 @@ void channel_funding_spent_watch_revert(struct lightningd *ld, const char *suffix, u32 blockheight) { - /* TODO: roll back onchaind state on funding-spend reorg. The full - * rollback (kill onchaind, restore CHANNELD_NORMAL, replay watches) - * needs onchaind itself to be running on bwatch first; until then, - * just record the event. */ u64 dbid = strtoull(suffix, NULL, 10); struct channel *channel = channel_by_dbid(ld, dbid); if (!channel) { - log_debug(ld->log, - "channel/funding_spent revert: unknown dbid %"PRIu64 - " at block %u, ignoring", - dbid, blockheight); + log_broken(ld->log, + "channel/funding_spent revert: unknown dbid %"PRIu64 + ", ignoring", + dbid); + return; + } + + /* bwatch fires this revert whenever the funding-confirmation block is + * reorged away, regardless of whether the outpoint was actually spent + * (start_block on this watch is the funding confirmation height, not + * the spend height). funding_spend_txid is only set once we've seen + * a spend, so its absence means there's nothing to roll back. */ + if (!channel->funding_spend_txid) { + log_debug(channel->log, + "Funding spend revert at block %u in state %s:" + " outpoint never spent, ignoring", + blockheight, channel_state_name(channel)); + return; + } + + /* Already in ONCHAIN means onchaind has fully taken over. We don't + * have a sane way to undo that, and in practice this revert during + * ONCHAIN almost always means a startup resync (watchman height < + * bwatch tip), not a real deep reorg of the spending tx. */ + if (channel->state == ONCHAIN) { + log_unusual(channel->log, + "Funding spend revert at block %u while ONCHAIN:" + " ignoring (likely startup resync, not a real reorg)", + blockheight); return; } log_unusual(channel->log, - "channel/funding_spent revert at block %u (state %s):" - " no rollback yet", + "Funding spend reorged out at block %u (state %s) --" + " rolling back", blockheight, channel_state_name(channel)); + + /* Kill onchaind first so it stops touching state we're about to roll + * back. */ + channel_set_owner(channel, NULL); + + /* Drop bwatch watches before clearing close_blockheight: the unwatch + * for the channel_close depth watch needs that height to compute the + * matching owner. */ + onchaind_clear_watches(channel); + channel->close_blockheight = tal_free(channel->close_blockheight); + + /* Reset gossip before channel_set_state persists the new state: there + * is no legal backward gossip transition from ONCHAIN. */ + channel_gossip_funding_reorg(channel); + + channel_set_state(channel, channel->state, CHANNELD_NORMAL, + REASON_UNKNOWN, "Funding spend reorged out"); } void channel_wrong_funding_spent_watch_found(struct lightningd *ld, From f92febf0523da88a6016460df325e42646126603 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 09:35:22 +0930 Subject: [PATCH 64/77] lightningd: add inert onchaind bwatch handlers Stages every handler bwatch will call once we flip onchaind onto it. Nothing dispatches to them yet, so behaviour is unchanged and the existing replay path is still in charge. Three watch flavours, all keyed by the spending tx: onchaind/outpoint// Found: a watched output was spent; forward to onchaind. Revert: drop the entry (the spent message we sent can't be recalled, but onchaind recovers from depth updates on the re-mined block). onchaind/depth// Per-tx depth ticks for CSV / HTLC maturity. One per tracked tx. onchaind/channel_close/: Persistent restart marker. Normally a no-op; on crash recovery (onchaind not running) it looks the spending tx up in our_txs and re-launches. Saves us a "is this channel closing?" DB column. onchaind_send_depth_updates is the per-block driver that channel_block_processed will call to push depths to every tracked tx in one pass. --- lightningd/onchain_control.c | 258 +++++++++++++++++++++++++++++++++-- lightningd/onchain_control.h | 42 ++++++ 2 files changed, 290 insertions(+), 10 deletions(-) diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c index b5e26662a3a5..6dec0016feb2 100644 --- a/lightningd/onchain_control.c +++ b/lightningd/onchain_control.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -81,19 +82,11 @@ HTABLE_DEFINE_NODUPS_TYPE(struct onchaind_watched_tx, onchaind_watched_tx_eq_txid, onchaind_tx_map); -/* Silence -Wunused-function for the generated _add until the producer side - * (onchaind_funding_spent / bwatch_watch_outpoints) is wired up later. */ -static void UNUSED onchaind_tx_map_silence_unused(struct onchaind_tx_map *m, - struct onchaind_watched_tx *e) -{ - onchaind_tx_map_add(m, e); -} - /* Drop all bwatch watches we registered for this entry: the per-tx blockdepth * watch and every outpoint watch under the same owner. Idempotent on the * outpoint side because bwatch tolerates duplicate deletes. */ -static void UNUSED unwatch_entry(struct channel *channel, - struct onchaind_watched_tx *entry) +static void unwatch_entry(struct channel *channel, + struct onchaind_watched_tx *entry) { struct lightningd *ld = channel->peer->ld; const char *owner_out = owner_onchaind_outpoint(tmpctx, channel->dbid, @@ -425,6 +418,251 @@ static void watch_tx_and_outputs(struct channel *channel, onchain_txo_watched); } +/* bwatch handlers for onchaind tracking. + * + * The map is created lazily on the first bwatch_watch_outpoints call so + * channels that never close pay nothing. Per-tx ownership lets us tear + * watches down precisely on reorg without iterating the whole table. */ + +static void UNUSED bwatch_watch_outpoints(struct channel *channel, + const struct bitcoin_txid *txid, + u32 blockheight, + const struct bitcoin_outpoint *outpoints, + size_t num_outpoints) +{ + struct lightningd *ld = channel->peer->ld; + struct onchaind_watched_tx *entry; + + entry = onchaind_tx_map_get(channel->onchaind_watches, txid); + if (!entry) { + entry = tal(channel->onchaind_watches, struct onchaind_watched_tx); + entry->txid = *txid; + entry->blockheight = blockheight; + entry->outpoints = tal_arr(entry, struct bitcoin_outpoint, 0); + onchaind_tx_map_add(channel->onchaind_watches, entry); + + /* Single per-tx depth watch drives both CSV and HTLC maturity + * checks (both just need the depth). Removed on reorg via the + * revert handler and on full resolution via onchaind_clear_watches. */ + watchman_watch_blockdepth(ld, + owner_onchaind_depth(tmpctx, channel->dbid, txid), + blockheight); + /* Send the real depth straight away so a just-restarted onchaind + * makes correct CSV decisions instead of starting from zero. */ + u32 cur = get_block_height(ld->topology); + u32 depth = cur > blockheight ? cur - blockheight + 1 : 1; + onchain_tx_depth(channel, txid, depth); + } + + const char *owner_out = owner_onchaind_outpoint(tmpctx, channel->dbid, txid); + for (size_t i = 0; i < num_outpoints; i++) { + tal_arr_expand(&entry->outpoints, outpoints[i]); + watchman_watch_outpoint(ld, owner_out, &outpoints[i], blockheight); + } +} + +void onchaind_output_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t innum, + u32 blockheight, + u32 txindex UNUSED) +{ + u64 dbid = strtoull(suffix, NULL, 10); + struct channel *channel = channel_by_dbid(ld, dbid); + + if (!channel) { + log_broken(ld->log, + "onchaind/outpoint watch_found: unknown channel dbid %"PRIu64, + dbid); + return; + } + + if (!channel->owner) { + log_broken(channel->log, + "onchaind/outpoint watch_found: onchaind not running"); + return; + } + + onchain_txo_spent(channel, tx, innum, blockheight); +} + +void onchaind_output_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight) +{ + const char *sep = strchr(suffix, '/'); + struct bitcoin_txid txid; + struct onchaind_watched_tx *entry; + u64 dbid; + struct channel *channel; + + if (!sep) { + log_broken(ld->log, + "onchaind/outpoint revert: bad suffix %s", suffix); + return; + } + + dbid = strtoull(suffix, NULL, 10); + channel = channel_by_dbid(ld, dbid); + if (!channel || !channel->onchaind_watches) + return; + + if (!hex_decode(sep + 1, strlen(sep + 1), &txid, sizeof(txid))) { + log_broken(ld->log, + "onchaind/outpoint revert: bad txid in suffix %s", + suffix); + return; + } + + entry = onchaind_tx_map_get(channel->onchaind_watches, &txid); + /* Already reverted, or this txid was never tracked: nothing to do. + * The WIRE_ONCHAIND_SPENT message we sent can't be recalled, but + * onchaind will see the re-mined block and recover via depth updates. */ + if (!entry) + return; + + log_unusual(channel->log, + "onchaind-tracked output spend reorged out at block %u", + blockheight); + + unwatch_entry(channel, entry); + onchaind_tx_map_delkey(channel->onchaind_watches, &txid); + tal_free(entry); +} + +void onchaind_send_depth_updates(struct channel *channel, u32 blockheight) +{ + struct onchaind_watched_tx *entry; + struct onchaind_tx_map_iter rit; + + if (!channel->onchaind_watches) + return; + + for (entry = onchaind_tx_map_first(channel->onchaind_watches, &rit); + entry; + entry = onchaind_tx_map_next(channel->onchaind_watches, &rit)) { + u32 depth = (blockheight >= entry->blockheight) + ? (blockheight - entry->blockheight + 1) : 0; + onchain_tx_depth(channel, &entry->txid, depth); + } +} + +void onchaind_depth_found(struct lightningd *ld, + const char *suffix, + u32 depth, + u32 blockheight UNUSED) +{ + const char *sep = strchr(suffix, '/'); + u64 dbid; + struct bitcoin_txid txid; + struct channel *channel; + + if (!sep || !hex_decode(sep + 1, strlen(sep + 1), &txid, sizeof(txid))) { + log_broken(ld->log, "onchaind/depth bad suffix: %s", suffix); + return; + } + dbid = strtoull(suffix, NULL, 10); + channel = channel_by_dbid(ld, dbid); + if (!channel || !channel->owner) + return; + + onchain_tx_depth(channel, &txid, depth); +} + +void onchaind_depth_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight) +{ + const char *sep = strchr(suffix, '/'); + u64 dbid; + struct bitcoin_txid txid; + + if (!sep || !hex_decode(sep + 1, strlen(sep + 1), &txid, sizeof(txid))) { + log_broken(ld->log, "onchaind/depth revert bad suffix: %s", + suffix); + return; + } + dbid = strtoull(suffix, NULL, 10); + watchman_unwatch_blockdepth(ld, + owner_onchaind_depth(tmpctx, dbid, &txid), + blockheight); +} + +/* Restart marker. Fires every block from the funding-spend block until + * deleted in handle_irrevocably_resolved. Normally a no-op (onchaind is + * already running and gets per-tx depths via onchaind/depth/...); the + * real work is the crash-recovery branch when channel->owner is NULL. */ +void onchaind_channel_close_depth_found(struct lightningd *ld, + const char *suffix, + u32 depth UNUSED, + u32 blockheight UNUSED) +{ + const char *txid_hex; + u64 dbid; + struct bitcoin_txid txid; + struct channel *channel; + struct bitcoin_tx *tx; + u32 spend_blockheight; + + /* suffix is ":" */ + txid_hex = strchr(suffix, ':'); + if (!txid_hex) { + log_broken(ld->log, + "onchaind/channel_close: malformed suffix '%s'", + suffix); + return; + } + txid_hex++; + + dbid = strtoull(suffix, NULL, 10); + channel = channel_by_dbid(ld, dbid); + if (!channel) + return; + + if (channel->owner) + return; + + if (!hex_decode(txid_hex, strlen(txid_hex), &txid, sizeof(txid)) + || strlen(txid_hex) != sizeof(txid) * 2) { + log_broken(channel->log, + "onchaind/channel_close: bad txid hex in suffix '%s'", + suffix); + return; + } + + tx = wallet_transaction_get(tmpctx, ld->wallet, &txid); + if (!tx) { + log_broken(channel->log, + "onchaind/channel_close: spending tx not in our_txs"); + return; + } + + spend_blockheight = channel->close_blockheight + ? *channel->close_blockheight + : wallet_transaction_height(ld->wallet, &txid); + if (!spend_blockheight) { + log_broken(channel->log, + "onchaind/channel_close: spend blockheight not found for %s", + fmt_bitcoin_txid(tmpctx, &txid)); + return; + } + + log_info(channel->log, + "Restarting onchaind after crash (channel_close watch fired)"); + onchaind_funding_spent(channel, tx, spend_blockheight); +} + +void onchaind_channel_close_depth_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight) +{ + watchman_unwatch_blockdepth(ld, + tal_fmt(tmpctx, "onchaind/channel_close/%s", + suffix), + blockheight); +} + static void handle_onchain_log_coin_move(struct channel *channel, const u8 *msg) { struct chain_coin_mvt *mvt = tal(NULL, struct chain_coin_mvt); diff --git a/lightningd/onchain_control.h b/lightningd/onchain_control.h index 2da32139479a..e79e1661977e 100644 --- a/lightningd/onchain_control.h +++ b/lightningd/onchain_control.h @@ -18,4 +18,46 @@ void onchaind_replay_channels(struct lightningd *ld); * closing) or when we lose track of an onchaind session for any reason. */ void onchaind_clear_watches(struct channel *channel); +/* bwatch handler "onchaind/outpoint//": one of the outputs of a tx + * onchaind asked us to watch was spent. Forwards the spending tx to onchaind. */ +void onchaind_output_watch_found(struct lightningd *ld, + const char *suffix, + const struct bitcoin_tx *tx, + size_t innum, + u32 blockheight, + u32 txindex); + +/* Revert: the spending tx was reorged away. Drops the entry; onchaind will + * recover from the re-mined block via its normal depth updates. */ +void onchaind_output_watch_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + +/* Per-block depth driver: pushes the current depth of every tx onchaind is + * tracking. Called from channel_block_processed. */ +void onchaind_send_depth_updates(struct channel *channel, u32 blockheight); + +/* bwatch depth handler "onchaind/depth//": delivers the tx's + * depth to onchaind for CSV / HTLC maturity gates. */ +void onchaind_depth_found(struct lightningd *ld, + const char *suffix, + u32 depth, + u32 blockheight); + +void onchaind_depth_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + +/* bwatch depth handler "onchaind/channel_close/:": persistent + * restart marker. Normally a no-op; on crash recovery (channel->owner NULL) + * looks up the spending tx in our_txs and re-launches onchaind. */ +void onchaind_channel_close_depth_found(struct lightningd *ld, + const char *suffix, + u32 depth, + u32 blockheight); + +void onchaind_channel_close_depth_revert(struct lightningd *ld, + const char *suffix, + u32 blockheight); + #endif /* LIGHTNING_LIGHTNINGD_ONCHAIN_CONTROL_H */ From b9d60c434715cd462e85e6d4da1f8218f3ba1663 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 10:08:16 +0930 Subject: [PATCH 65/77] lightningd: drive onchaind from bwatch Switch onchaind_funding_spent over to bwatch: stash the spending tx in our_txs + funding_spend_txid, register the channel_close depth marker, and hand the commitment outputs to bwatch_watch_outpoints instead of replay_watch_tx / watch_tx_and_outputs. channel_block_processed now pumps onchaind_send_depth_updates each block for ONCHAIN/FUNDING_SPEND_SEEN channels, and watchman dispatch routes onchaind/{outpoint,depth,channel_close}/ to the handlers staged last commit. The replay path and onchain_txo_spent's subd_req+reply are dead but stay one more commit for an isolated cleanup diff. --- lightningd/onchain_control.c | 65 ++++++++++++++------- lightningd/onchain_control.h | 6 +- lightningd/peer_control.c | 5 +- lightningd/test/run-invoice-select-inchan.c | 25 +++++++- lightningd/watchman.c | 11 ++++ wallet/test/run-wallet.c | 12 +++- 6 files changed, 94 insertions(+), 30 deletions(-) diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c index 6dec0016feb2..66994c1da127 100644 --- a/lightningd/onchain_control.c +++ b/lightningd/onchain_control.c @@ -424,11 +424,11 @@ static void watch_tx_and_outputs(struct channel *channel, * channels that never close pay nothing. Per-tx ownership lets us tear * watches down precisely on reorg without iterating the whole table. */ -static void UNUSED bwatch_watch_outpoints(struct channel *channel, - const struct bitcoin_txid *txid, - u32 blockheight, - const struct bitcoin_outpoint *outpoints, - size_t num_outpoints) +static void bwatch_watch_outpoints(struct channel *channel, + const struct bitcoin_txid *txid, + u32 blockheight, + const struct bitcoin_outpoint *outpoints, + size_t num_outpoints) { struct lightningd *ld = channel->peer->ld; struct onchaind_watched_tx *entry; @@ -2084,11 +2084,12 @@ static void onchain_error(struct channel *channel, /* With a reorg, this can get called multiple times; each time we'll kill * onchaind (like any other owner), and restart */ -enum watch_result onchaind_funding_spent(struct channel *channel, - const struct bitcoin_tx *tx, - u32 blockheight) +void onchaind_funding_spent(struct channel *channel, + const struct bitcoin_tx *tx, + u32 blockheight) { u8 *msg; + struct bitcoin_txid funding_spend_txid; struct bitcoin_txid our_last_txid; struct lightningd *ld = channel->peer->ld; int hsmfd; @@ -2114,6 +2115,7 @@ enum watch_result onchaind_funding_spent(struct channel *channel, tal_free(channel->close_blockheight); channel->close_blockheight = tal_dup(channel, u32, &blockheight); + bitcoin_txid(tx, &funding_spend_txid); /* We could come from almost any state. */ /* NOTE(mschmoock) above comment is wrong, since we failed above! */ @@ -2123,6 +2125,24 @@ enum watch_result onchaind_funding_spent(struct channel *channel, reason, tal_fmt(tmpctx, "Onchain funding spend")); + /* Stash the spending tx in our_txs so the channel_close depth handler + * can resurrect onchaind across restarts without a dedicated DB column. */ + wallet_transaction_add(ld->wallet, tx->wtx, blockheight, 0); + + /* In-memory only: lets onchaind_clear_watches and the funding-spent + * revert build the channel_close owner string for unwatch. */ + channel->funding_spend_txid + = tal_dup(channel, struct bitcoin_txid, &funding_spend_txid); + + /* Persistent restart marker. Fires every block until + * handle_irrevocably_resolved unregisters it; on restart the handler + * sees channel->owner == NULL and re-launches onchaind. */ + watchman_watch_blockdepth(ld, + owner_onchaind_channel_close(tmpctx, + channel->dbid, + &funding_spend_txid), + blockheight); + hsmfd = hsm_get_client_fd(ld, &channel->peer->id, channel->dbid, HSM_PERM_SIGN_ONCHAIN_TX @@ -2130,7 +2150,7 @@ enum watch_result onchaind_funding_spent(struct channel *channel, if (hsmfd < 0) { log_broken(channel->log, "Could not get hsm fd for onchaind: %s", strerror(errno)); - return KEEP_WATCHING; + return; } channel_set_owner(channel, new_channel_subd(channel, ld, @@ -2148,7 +2168,7 @@ enum watch_result onchaind_funding_spent(struct channel *channel, if (!channel->owner) { log_broken(channel->log, "Could not subdaemon onchain: %s", strerror(errno)); - return KEEP_WATCHING; + return; } struct ext_key final_wallet_ext_key; @@ -2159,7 +2179,7 @@ enum watch_result onchaind_funding_spent(struct channel *channel, &final_wallet_ext_key) != WALLY_OK) { log_broken(channel->log, "Could not derive final_wallet_ext_key %"PRIu64, channel->final_key_idx); - return KEEP_WATCHING; + return; } /* This could be a mutual close, but it doesn't matter. @@ -2215,15 +2235,20 @@ enum watch_result onchaind_funding_spent(struct channel *channel, feerate_min(ld, NULL)); subd_send_msg(channel->owner, take(msg)); - /* If we're replaying, we just watch this */ - if (channel->onchaind_replay_watches) { - replay_watch_tx(channel, blockheight, tx); - } else { - watch_tx_and_outputs(channel, tx); - } - - /* We keep watching until peer finally deleted, for reorgs. */ - return KEEP_WATCHING; + if (!channel->onchaind_watches) + channel->onchaind_watches = new_htable(channel, onchaind_tx_map); + + /* For the commitment tx itself we watch every output up front: onchaind + * will resolve each one and tell us via WIRE_ONCHAIND_WATCH_OUTPOINTS + * what to watch from there on (HTLC sweeps, second-stage txs, ...). */ + struct bitcoin_outpoint *all_outputs; + all_outputs = tal_arr(tmpctx, struct bitcoin_outpoint, tx->wtx->num_outputs); + for (u32 n = 0; n < tx->wtx->num_outputs; n++) { + all_outputs[n].txid = funding_spend_txid; + all_outputs[n].n = n; + } + bwatch_watch_outpoints(channel, &funding_spend_txid, blockheight, + all_outputs, tx->wtx->num_outputs); } void onchaind_replay_channels(struct lightningd *ld) diff --git a/lightningd/onchain_control.h b/lightningd/onchain_control.h index e79e1661977e..b38c8830c8f9 100644 --- a/lightningd/onchain_control.h +++ b/lightningd/onchain_control.h @@ -7,9 +7,9 @@ struct channel; struct bitcoin_tx; struct block; -enum watch_result onchaind_funding_spent(struct channel *channel, - const struct bitcoin_tx *tx, - u32 blockheight); +void onchaind_funding_spent(struct channel *channel, + const struct bitcoin_tx *tx, + u32 blockheight); void onchaind_replay_channels(struct lightningd *ld); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index cf0e21984b08..72e9d3bd52e2 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -2516,8 +2516,11 @@ void channel_block_processed(struct lightningd *ld, u32 blockheight) /* onchaind drives its own per-tx depth tracking. */ if (channel->state == ONCHAIN - || channel->state == FUNDING_SPEND_SEEN) + || channel->state == FUNDING_SPEND_SEEN) { + if (channel->owner) + onchaind_send_depth_updates(channel, blockheight); continue; + } /* Skip unconfirmed channels; stub scids are zero-conf placeholders. */ if (!channel->scid || is_stub_scid(*channel->scid)) diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index 18596dee0a25..8ed2322851ca 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -94,6 +94,9 @@ void channel_fail_transient(struct channel *channel UNNEEDED, /* Generated stub for channel_gossip_channel_disconnect */ void channel_gossip_channel_disconnect(struct channel *channel UNNEEDED) { fprintf(stderr, "channel_gossip_channel_disconnect called!\n"); abort(); } +/* Generated stub for channel_gossip_funding_reorg */ +void channel_gossip_funding_reorg(struct channel *channel UNNEEDED) +{ fprintf(stderr, "channel_gossip_funding_reorg called!\n"); abort(); } /* Generated stub for channel_gossip_get_remote_update */ const struct peer_update *channel_gossip_get_remote_update(const struct channel *channel UNNEEDED) { fprintf(stderr, "channel_gossip_get_remote_update called!\n"); abort(); } @@ -123,9 +126,19 @@ void channel_set_last_tx(struct channel *channel UNNEEDED, struct bitcoin_tx *tx UNNEEDED, const struct bitcoin_signature *sig UNNEEDED) { fprintf(stderr, "channel_set_last_tx called!\n"); abort(); } +/* Generated stub for channel_set_owner */ +void channel_set_owner(struct channel *channel UNNEEDED, struct subd *owner UNNEEDED) +{ fprintf(stderr, "channel_set_owner called!\n"); abort(); } /* Generated stub for channel_set_scid */ void channel_set_scid(struct channel *channel UNNEEDED, const struct short_channel_id *new_scid UNNEEDED) { fprintf(stderr, "channel_set_scid called!\n"); abort(); } +/* Generated stub for channel_set_state */ +void channel_set_state(struct channel *channel UNNEEDED, + enum channel_state old_state UNNEEDED, + enum channel_state state UNNEEDED, + enum state_change reason UNNEEDED, + const char *why UNNEEDED) +{ fprintf(stderr, "channel_set_state called!\n"); abort(); } /* Generated stub for channel_state_name */ const char *channel_state_name(const struct channel *channel UNNEEDED) { fprintf(stderr, "channel_state_name called!\n"); abort(); } @@ -551,11 +564,17 @@ void notify_invoice_payment(struct lightningd *ld UNNEEDED, const struct json_escape *label UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED) { fprintf(stderr, "notify_invoice_payment called!\n"); abort(); } +/* Generated stub for onchaind_clear_watches */ +void onchaind_clear_watches(struct channel *channel UNNEEDED) +{ fprintf(stderr, "onchaind_clear_watches called!\n"); abort(); } /* Generated stub for onchaind_funding_spent */ -enum watch_result onchaind_funding_spent(struct channel *channel UNNEEDED, - const struct bitcoin_tx *tx UNNEEDED, - u32 blockheight UNNEEDED) +void onchaind_funding_spent(struct channel *channel UNNEEDED, + const struct bitcoin_tx *tx UNNEEDED, + u32 blockheight UNNEEDED) { fprintf(stderr, "onchaind_funding_spent called!\n"); abort(); } +/* Generated stub for onchaind_send_depth_updates */ +void onchaind_send_depth_updates(struct channel *channel UNNEEDED, u32 blockheight UNNEEDED) +{ fprintf(stderr, "onchaind_send_depth_updates called!\n"); abort(); } /* Generated stub for param_index */ struct command_result *param_index(struct command *cmd UNNEEDED, const char *name UNNEEDED, const char *buffer UNNEEDED, diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 7258e9f5eccd..5f55e0f95ff3 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -469,6 +470,13 @@ static const struct depth_dispatch { /* channel/funding_depth/: WATCH_BLOCKDEPTH, fires once per new block * while the funding tx accumulates confirmations. */ { "channel/funding_depth/", channel_funding_depth_found, channel_funding_depth_revert }, + /* onchaind/channel_close/:: WATCH_BLOCKDEPTH, persistent restart + * marker for a closing channel. Normally a no-op; on crash recovery + * (channel->owner == NULL) the handler relaunches onchaind. */ + { "onchaind/channel_close/", onchaind_channel_close_depth_found, onchaind_channel_close_depth_revert }, + /* onchaind/depth//: WATCH_BLOCKDEPTH, per-tx depth ticks that + * drive CSV and HTLC maturity checks inside onchaind. */ + { "onchaind/depth/", onchaind_depth_found, onchaind_depth_revert }, { NULL, NULL, NULL }, }; @@ -499,6 +507,9 @@ static const struct watch_dispatch { /* channel/funding/: WATCH_SCRIPTPUBKEY, fires when the funding output script * appears in a tx (i.e. the channel's funding transaction has been confirmed). */ { "channel/funding/", channel_funding_watch_found, channel_funding_watch_revert }, + /* onchaind/outpoint//: WATCH_OUTPOINT, fires when an output + * onchaind asked us to track is spent (HTLC sweep, second-stage tx, ...). */ + { "onchaind/outpoint/", onchaind_output_watch_found, onchaind_output_watch_revert }, { NULL, NULL, NULL }, }; diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 1f5aa62c5d57..0cd9dfa73bbf 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -564,11 +564,17 @@ void notify_sendpay_failure(struct lightningd *ld UNNEEDED, void notify_sendpay_success(struct lightningd *ld UNNEEDED, const struct wallet_payment *payment UNNEEDED) { fprintf(stderr, "notify_sendpay_success called!\n"); abort(); } +/* Generated stub for onchaind_clear_watches */ +void onchaind_clear_watches(struct channel *channel UNNEEDED) +{ fprintf(stderr, "onchaind_clear_watches called!\n"); abort(); } /* Generated stub for onchaind_funding_spent */ -enum watch_result onchaind_funding_spent(struct channel *channel UNNEEDED, - const struct bitcoin_tx *tx UNNEEDED, - u32 blockheight UNNEEDED) +void onchaind_funding_spent(struct channel *channel UNNEEDED, + const struct bitcoin_tx *tx UNNEEDED, + u32 blockheight UNNEEDED) { fprintf(stderr, "onchaind_funding_spent called!\n"); abort(); } +/* Generated stub for onchaind_send_depth_updates */ +void onchaind_send_depth_updates(struct channel *channel UNNEEDED, u32 blockheight UNNEEDED) +{ fprintf(stderr, "onchaind_send_depth_updates called!\n"); abort(); } /* Generated stub for outpointfilter_add */ void outpointfilter_add(struct outpointfilter *of UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED) From b293f398d3784b47a2bb78bc6e6b937900059306 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 10:29:12 +0930 Subject: [PATCH 66/77] lightningd: drop onchaind chaintopology + replay machinery bwatch drives onchaind end-to-end now; the replay loop, the chaintopology txwatch callbacks, and the onchaind_spent reply round-trip are dead. --- lightningd/channel.c | 3 - lightningd/channel.h | 7 - lightningd/lightningd.c | 8 - lightningd/onchain_control.c | 319 +------------------------- lightningd/onchain_control.h | 2 - lightningd/test/run-find_my_abspath.c | 3 - onchaind/onchaind.c | 8 +- onchaind/onchaind_wire.csv | 4 - onchaind/test/run-grind_feerate.c | 3 - 9 files changed, 11 insertions(+), 346 deletions(-) diff --git a/lightningd/channel.c b/lightningd/channel.c index faa62bdd767e..3cb07d40aefe 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -420,7 +420,6 @@ struct channel *new_unsaved_channel(struct peer *peer, channel->ignore_fee_limits = ld->config.ignore_fee_limits; channel->last_stable_connection = 0; channel->stable_conn_timer = NULL; - channel->onchaind_replay_watches = NULL; /* Nothing happened yet */ memset(&channel->stats, 0, sizeof(channel->stats)); channel->state_changes = tal_arr(channel, struct channel_state_change *, 0); @@ -720,8 +719,6 @@ struct channel *new_channel(struct peer *peer, u64 dbid, channel->ignore_fee_limits = ignore_fee_limits; channel->last_stable_connection = last_stable_connection; channel->stable_conn_timer = NULL; - channel->onchaind_replay_watches = NULL; - channel->num_onchain_spent_calls = 0; channel->stats = *stats; channel->state_changes = tal_steal(channel, state_changes); diff --git a/lightningd/channel.h b/lightningd/channel.h index 65cd4944d9ed..092437c83a29 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -215,13 +215,6 @@ struct channel { * NULL when no splice detection is pending. */ struct bitcoin_outpoint *pre_splice_funding; - /* If we're doing a replay for onchaind, here are the txids it's watching */ - struct replay_tx_hash *onchaind_replay_watches; - /* Number of outstanding onchaind_spent calls */ - size_t num_onchain_spent_calls; - /* Height we're replaying at (if onchaind_replay_watches set) */ - u32 onchaind_replay_height; - /* Per-session map of txs onchaind has asked us to watch: * txid -> {confirm height, the outpoints we registered}. * Initialised by onchaind_funding_spent; NULL before onchaind starts. diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 5bedbc88b1e4..35ba0679f3ad 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -70,7 +70,6 @@ #include #include #include -#include #include #include #include @@ -1394,13 +1393,6 @@ int main(int argc, char *argv[]) * uninitialized data. */ connectd_activate(ld); - /*~ "onchaind" is a dumb daemon which tries to get our funds back: it - * doesn't handle reorganizations, but it's idempotent, so we can - * simply just restart it if the chain moves. Similarly, we replay it - * chain events from the database on restart, beginning with the - * "funding transaction spent" event which creates it. */ - onchaind_replay_channels(ld); - /*~ Now handle sigchld, so we can clean up appropriately. */ sigchld_conn = notleak(io_new_conn(ld, sigchld_rfd, sigchld_rfd_in, ld)); diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c index 66994c1da127..ca2f2ec2e574 100644 --- a/lightningd/onchain_control.c +++ b/lightningd/onchain_control.c @@ -24,27 +24,6 @@ #include #include -/* If we're restarting, we keep a per-channel copy of watches, and replay */ -struct replay_tx { - u32 blockheight; - struct bitcoin_txid txid; - struct bitcoin_tx *tx; -}; - -static const struct bitcoin_txid *replay_tx_keyof(const struct replay_tx *rtx) -{ - return &rtx->txid; -} - -static bool replay_tx_eq_txid(const struct replay_tx *rtx, - const struct bitcoin_txid *txid) -{ - return bitcoin_txid_eq(&rtx->txid, txid); -} - -HTABLE_DEFINE_NODUPS_TYPE(struct replay_tx, replay_tx_keyof, txid_hash, replay_tx_eq_txid, - replay_tx_hash); - /* Per-channel record of one tx onchaind is tracking. * - blockheight: confirmation height; needed to unwatch the matching * blockdepth watch on reorg or full resolution. @@ -270,152 +249,16 @@ static void onchain_tx_depth(struct channel *channel, subd_send_msg(channel->owner, take(msg)); } -/** - * Entrypoint for the txwatch callback, calls onchain_tx_depth. - */ -static enum watch_result onchain_tx_watched(struct lightningd *ld, - const struct bitcoin_txid *txid, - const struct bitcoin_tx *tx, - unsigned int depth, - struct channel *channel) -{ - u32 blockheight = get_block_height(ld->topology); - - if (tx != NULL) { - struct bitcoin_txid txid2; - - bitcoin_txid(tx, &txid2); - if (!bitcoin_txid_eq(txid, &txid2)) { - channel_internal_error(channel, "Txid for %s is not %s", - fmt_bitcoin_tx(tmpctx, tx), - fmt_bitcoin_txid(tmpctx, txid)); - return DELETE_WATCH; - } - } - - if (depth == 0) { - log_unusual(channel->log, "Chain reorganization!"); - channel_set_owner(channel, NULL); - - /* We will most likely be freed, so this is a noop */ - return KEEP_WATCHING; - } - - /* Store so we remember if we crash, and can replay later */ - wallet_insert_funding_spend(ld->wallet, channel, txid, 0, blockheight); - - onchain_tx_depth(channel, txid, depth); - return KEEP_WATCHING; -} - -static void watch_tx_and_outputs(struct channel *channel, - const struct bitcoin_tx *tx); -static void onchaind_replay(struct channel *channel); - -static void replay_unwatch_txid(struct channel *channel, - const struct bitcoin_txid *txid) -{ - replay_tx_hash_delkey(channel->onchaind_replay_watches, txid); -} - -static void onchaind_spent_reply(struct subd *onchaind, const u8 *msg, - const int *fds, - struct bitcoin_txid *txid) -{ - bool interested; - struct txwatch *txw; - struct channel *channel = onchaind->channel; - - if (!fromwire_onchaind_spent_reply(msg, &interested)) - channel_internal_error(channel, "Invalid onchaind_spent_reply %s", - tal_hex(tmpctx, msg)); - - channel->num_onchain_spent_calls--; - - /* Only delete watch if it says it doesn't care */ - if (interested) - goto out; - - /* If we're doing replay: */ - if (channel->onchaind_replay_watches) { - replay_unwatch_txid(channel, txid); - goto out; - } - - /* Frees the txo watches, too: see watch_tx_and_outputs() */ - txw = find_txwatch(channel->peer->ld->topology, txid, - onchain_tx_watched, channel); - if (!txw) - log_unusual(channel->log, "Can't unwatch txid %s", - fmt_bitcoin_txid(tmpctx, txid)); - tal_free(txw); - -out: - /* If that's the last request, continue asking for blocks */ - if (channel->onchaind_replay_watches - && channel->num_onchain_spent_calls == 0) { - onchaind_replay(channel); - } -} - -/** - * Notify onchaind that an output was spent and register new watches. - */ -static void onchain_txo_spent(struct channel *channel, const struct bitcoin_tx *tx, size_t input_num, u32 blockheight) -{ - u8 *msg; - struct bitcoin_txid *txid; - /* Onchaind needs all inputs, since it uses those to compare - * with existing spends (which can vary, with feerate changes). */ - struct tx_parts *parts = tx_parts_from_wally_tx(tmpctx, tx->wtx, - -1, -1); - - watch_tx_and_outputs(channel, tx); - - /* Reply will need this if we want to unwatch */ - txid = tal(NULL, struct bitcoin_txid); - bitcoin_txid(tx, txid); - - msg = towire_onchaind_spent(channel, parts, input_num, blockheight); - subd_req(channel->owner, channel->owner, take(msg), -1, 0, - onchaind_spent_reply, take(txid)); - channel->num_onchain_spent_calls++; -} - -/** - * Entrypoint for the txowatch callback, stores tx and calls onchain_txo_spent. - */ -static enum watch_result onchain_txo_watched(struct channel *channel, - const struct bitcoin_tx *tx, - size_t input_num, - const struct block *block) -{ - onchain_txo_spent(channel, tx, input_num, block->height); - - /* We don't need to keep watching: If this output is double-spent - * (reorg), we'll get a zero depth cb to onchain_tx_watched, and - * restart onchaind. */ - return DELETE_WATCH; -} - -/* To avoid races, we watch the tx and all outputs. */ -static void watch_tx_and_outputs(struct channel *channel, - const struct bitcoin_tx *tx) +/* Forward an output-spend notification to onchaind. bwatch is in charge + * of (un)watching, so this no longer needs a reply round-trip. */ +static void onchain_txo_spent(struct channel *channel, + const struct bitcoin_tx *tx, + size_t input_num, + u32 blockheight) { - struct bitcoin_outpoint outpoint; - struct txwatch *txw; - struct lightningd *ld = channel->peer->ld; - - bitcoin_txid(tx, &outpoint.txid); - - /* Make txwatch a parent of txo watches, so we can unwatch together. */ - txw = watch_txid(channel->owner, ld->topology, - &outpoint.txid, - onchain_tx_watched, channel); - - for (outpoint.n = 0; outpoint.n < tx->wtx->num_outputs; outpoint.n++) - watch_txo(txw, ld->topology, channel, &outpoint, - onchain_txo_watched); + struct tx_parts *parts = tx_parts_from_wally_tx(tmpctx, tx->wtx, -1, -1); + u8 *msg = towire_onchaind_spent(channel, parts, input_num, blockheight); + subd_send_msg(channel->owner, take(msg)); } /* bwatch handlers for onchaind tracking. @@ -699,108 +542,6 @@ static void handle_onchain_log_penalty_adj(struct channel *channel, const u8 *ms wallet_save_channel_mvt(channel->peer->ld, mvt); } -static void replay_watch_tx(struct channel *channel, - u32 blockheight, - const struct bitcoin_tx *tx TAKES) -{ - struct replay_tx *rtx = tal(channel->onchaind_replay_watches, struct replay_tx); - bitcoin_txid(tx, &rtx->txid); - rtx->blockheight = blockheight; - rtx->tx = clone_bitcoin_tx(rtx, tx); - - /* We might already be watching, in which case don't re-add! */ - if (replay_tx_hash_get(channel->onchaind_replay_watches, &rtx->txid)) - tal_free(rtx); - else - replay_tx_hash_add(channel->onchaind_replay_watches, rtx); -} - -/* We've finished replaying, turn any txs left into live watches */ -static void convert_replay_txs(struct channel *channel) -{ - struct replay_tx *rtx; - struct replay_tx_hash_iter rit; - struct replay_tx_hash *watches; - - /* Set to NULL so these are queued as real watches */ - watches = tal_steal(tmpctx, channel->onchaind_replay_watches); - channel->onchaind_replay_watches = NULL; - replay_tx_hash_lock(watches); - for (rtx = replay_tx_hash_first(watches, &rit); - rtx; - rtx = replay_tx_hash_next(watches, &rit)) { - watch_tx_and_outputs(channel, rtx->tx); - } - replay_tx_hash_unlock(watches); -} - -static void replay_block(struct bitcoind *bitcoind, - u32 height, - struct bitcoin_blkid *blkid, - struct bitcoin_block *blk, - struct channel *channel) -{ - struct replay_tx *rtx; - struct replay_tx_hash_iter rit; - - /* If we're shutting down, this can happen! */ - if (!channel->owner) - return; - - /* Tell onchaind that all existing txs have reached a new depth */ - replay_tx_hash_lock(channel->onchaind_replay_watches); - for (rtx = replay_tx_hash_first(channel->onchaind_replay_watches, &rit); - rtx; - rtx = replay_tx_hash_next(channel->onchaind_replay_watches, &rit)) { - /* Note: if you're in this block, that's depth 1! */ - onchain_tx_depth(channel, &rtx->txid, height - rtx->blockheight + 1); - } - replay_tx_hash_unlock(channel->onchaind_replay_watches); - - /* See if we add any new txs which spend a watched one */ - for (size_t i = 0; i < tal_count(blk->tx); i++) { - for (size_t j = 0; j < blk->tx[i]->wtx->num_inputs; j++) { - struct bitcoin_txid spent; - bitcoin_tx_input_get_txid(blk->tx[i], j, &spent); - rtx = replay_tx_hash_get(channel->onchaind_replay_watches, &spent); - if (rtx) { - /* Note: for efficiency, blk->tx's don't have - * PSBTs, so add one now */ - if (!blk->tx[i]->psbt) - blk->tx[i]->psbt = new_psbt(blk->tx[i], blk->tx[i]->wtx); - onchain_txo_spent(channel, blk->tx[i], j, height); - /* Watch this and all the children too. */ - replay_watch_tx(channel, height, blk->tx[i]); - } - } - } - - /* Replay finished? Now we'll get fed real blocks */ - if (height == get_block_height(bitcoind->ld->topology)) { - convert_replay_txs(channel); - return; - } - - /* Ready for next block */ - channel->onchaind_replay_height = height + 1; - - /* Otherwise, wait for those to be resolved (in case onchaind is slow, - * e.g. waiting for HSM). */ - if (channel->num_onchain_spent_calls == 0) - onchaind_replay(channel); -} - -static void onchaind_replay(struct channel *channel) -{ - assert(channel->onchaind_replay_watches); - assert(channel->num_onchain_spent_calls == 0); - - bitcoind_getrawblockbyheight(channel, - channel->peer->ld->topology->bitcoind, - channel->onchaind_replay_height, - replay_block, channel); -} - static void handle_extracted_preimage(struct channel *channel, const u8 *msg) { struct preimage preimage; @@ -2056,7 +1797,6 @@ static unsigned int onchain_msg(struct subd *sd, const u8 *msg, const int *fds U case WIRE_ONCHAIND_SPEND_CREATED: case WIRE_ONCHAIND_DEV_MEMLEAK: case WIRE_ONCHAIND_DEV_MEMLEAK_REPLY: - case WIRE_ONCHAIND_SPENT_REPLY: break; } @@ -2250,44 +1990,3 @@ void onchaind_funding_spent(struct channel *channel, bwatch_watch_outpoints(channel, &funding_spend_txid, blockheight, all_outputs, tx->wtx->num_outputs); } - -void onchaind_replay_channels(struct lightningd *ld) -{ - struct peer *peer; - struct peer_node_id_map_iter it; - - /* We don't hold a db tx for all of init */ - db_begin_transaction(ld->wallet->db); - - /* For each channel, if we've recorded a spend, it's onchaind time! */ - for (peer = peer_node_id_map_first(ld->peers, &it); - peer; - peer = peer_node_id_map_next(ld->peers, &it)) { - struct channel *channel; - - list_for_each(&peer->channels, channel, list) { - struct bitcoin_tx *tx; - u32 blockheight; - - if (channel_state_uncommitted(channel->state)) - continue; - - tx = wallet_get_funding_spend(tmpctx, ld->wallet, channel->dbid, - &blockheight); - if (!tx) - continue; - - log_info(channel->log, - "Restarting onchaind (%s): closed in block %u", - channel_state_name(channel), blockheight); - - /* We're in replay mode */ - channel->onchaind_replay_watches = new_htable(channel, replay_tx_hash); - channel->onchaind_replay_height = blockheight; - - onchaind_funding_spent(channel, tx, blockheight); - onchaind_replay(channel); - } - } - db_commit_transaction(ld->wallet->db); -} diff --git a/lightningd/onchain_control.h b/lightningd/onchain_control.h index b38c8830c8f9..7f6d7135dbbb 100644 --- a/lightningd/onchain_control.h +++ b/lightningd/onchain_control.h @@ -11,8 +11,6 @@ void onchaind_funding_spent(struct channel *channel, const struct bitcoin_tx *tx, u32 blockheight); -void onchaind_replay_channels(struct lightningd *ld); - /* Tear down all bwatch watches that onchaind registered for this channel. * Called when the funding-spend tx is reorged out (channel is no longer * closing) or when we lose track of an onchaind session for any reason. */ diff --git a/lightningd/test/run-find_my_abspath.c b/lightningd/test/run-find_my_abspath.c index 2a41dd720fda..1e014855cda4 100644 --- a/lightningd/test/run-find_my_abspath.c +++ b/lightningd/test/run-find_my_abspath.c @@ -156,9 +156,6 @@ struct peer_fd *new_peer_fd_arr(const tal_t *ctx UNNEEDED, const int *fd UNNEEDE /* Generated stub for new_topology */ struct chain_topology *new_topology(struct lightningd *ld UNNEEDED, struct logger *log UNNEEDED) { fprintf(stderr, "new_topology called!\n"); abort(); } -/* Generated stub for onchaind_replay_channels */ -void onchaind_replay_channels(struct lightningd *ld UNNEEDED) -{ fprintf(stderr, "onchaind_replay_channels called!\n"); abort(); } /* Generated stub for plugin_hook_call_ */ bool plugin_hook_call_(struct lightningd *ld UNNEEDED, struct plugin_hook *hook UNNEEDED, diff --git a/onchaind/onchaind.c b/onchaind/onchaind.c index 9b9beb26a571..76ca08683dda 100644 --- a/onchaind/onchaind.c +++ b/onchaind/onchaind.c @@ -1586,16 +1586,13 @@ static void handle_onchaind_spent(struct tracked_output ***outs, const u8 *msg) { struct tx_parts *tx_parts; u32 input_num, tx_blockheight; - bool interesting; if (!fromwire_onchaind_spent(msg, msg, &tx_parts, &input_num, &tx_blockheight)) master_badmsg(WIRE_ONCHAIND_SPENT, msg); - interesting = output_spent(outs, tx_parts, input_num, tx_blockheight); - - /* Tell lightningd if it was interesting */ - wire_sync_write(REQ_FD, take(towire_onchaind_spent_reply(NULL, interesting))); + /* bwatch (un)watches outputs for us; we no longer report back. */ + output_spent(outs, tx_parts, input_num, tx_blockheight); } static void handle_onchaind_known_preimage(struct tracked_output ***outs, @@ -1655,7 +1652,6 @@ static void wait_for_resolved(struct tracked_output **outs) /* We send these, not receive! */ case WIRE_ONCHAIND_INIT_REPLY: - case WIRE_ONCHAIND_SPENT_REPLY: case WIRE_ONCHAIND_EXTRACTED_PREIMAGE: case WIRE_ONCHAIND_MISSING_HTLC_OUTPUT: case WIRE_ONCHAIND_HTLC_TIMEOUT: diff --git a/onchaind/onchaind_wire.csv b/onchaind/onchaind_wire.csv index 3c9c76bbaf5a..e60458320464 100644 --- a/onchaind/onchaind_wire.csv +++ b/onchaind/onchaind_wire.csv @@ -67,10 +67,6 @@ msgdata,onchaind_spent,tx,tx_parts, msgdata,onchaind_spent,input_num,u32, msgdata,onchaind_spent,blockheight,u32, -# onchaind->master: do we want to continue watching this? -msgtype,onchaind_spent_reply,5104 -msgdata,onchaind_spent_reply,interested,bool, - # master->onchaind: We will receive more than one of these, as depth changes. msgtype,onchaind_depth,5005 msgdata,onchaind_depth,txid,bitcoin_txid, diff --git a/onchaind/test/run-grind_feerate.c b/onchaind/test/run-grind_feerate.c index 1acff25e440b..e77543844c6f 100644 --- a/onchaind/test/run-grind_feerate.c +++ b/onchaind/test/run-grind_feerate.c @@ -96,9 +96,6 @@ u8 *towire_onchaind_spend_penalty(const tal_t *ctx UNNEEDED, const struct bitcoi /* Generated stub for towire_onchaind_spend_to_us */ u8 *towire_onchaind_spend_to_us(const tal_t *ctx UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED, struct amount_sat outpoint_amount UNNEEDED, u32 sequence UNNEEDED, u32 minblock UNNEEDED, u64 commit_num UNNEEDED, const u8 *wscript UNNEEDED) { fprintf(stderr, "towire_onchaind_spend_to_us called!\n"); abort(); } -/* Generated stub for towire_onchaind_spent_reply */ -u8 *towire_onchaind_spent_reply(const tal_t *ctx UNNEEDED, bool interested UNNEEDED) -{ fprintf(stderr, "towire_onchaind_spent_reply called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ int main(int argc, char *argv[]) From ba0b8e8f41b546393b87ab9e96cd35a3c95c86fa Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 11:18:40 +0930 Subject: [PATCH 67/77] lightningd: let onchaind register HTLC second-stage watches via bwatch When an HTLC output is spent, onchaind appends new tracked outputs that also need watching. Add WIRE_ONCHAIND_WATCH_OUTPOINTS so onchaind can hand those outpoints to lightningd, which feeds them into bwatch. --- lightningd/onchain_control.c | 23 +++++++++++++++++++++++ onchaind/onchaind.c | 18 +++++++++++++++++- onchaind/onchaind_wire.csv | 8 ++++++++ onchaind/test/run-grind_feerate.c | 3 +++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c index ca2f2ec2e574..c9b59be42305 100644 --- a/lightningd/onchain_control.c +++ b/lightningd/onchain_control.c @@ -1719,6 +1719,25 @@ static void handle_onchaind_spend_htlc_expired(struct channel *channel, __func__); } +static void handle_onchaind_watch_outpoints(struct channel *channel, + const u8 *msg) +{ + struct bitcoin_txid txid; + u32 blockheight; + struct bitcoin_outpoint *outpoints; + + if (!fromwire_onchaind_watch_outpoints(tmpctx, msg, &txid, &blockheight, + &outpoints)) { + channel_internal_error(channel, + "Invalid onchaind_watch_outpoints %s", + tal_hex(tmpctx, msg)); + return; + } + + bwatch_watch_outpoints(channel, &txid, blockheight, + outpoints, tal_count(outpoints)); +} + static unsigned int onchain_msg(struct subd *sd, const u8 *msg, const int *fds UNUSED) { enum onchaind_wire t = fromwire_peektype(msg); @@ -1728,6 +1747,10 @@ static unsigned int onchain_msg(struct subd *sd, const u8 *msg, const int *fds U handle_onchain_init_reply(sd->channel, msg); break; + case WIRE_ONCHAIND_WATCH_OUTPOINTS: + handle_onchaind_watch_outpoints(sd->channel, msg); + break; + case WIRE_ONCHAIND_EXTRACTED_PREIMAGE: handle_extracted_preimage(sd->channel, msg); break; diff --git a/onchaind/onchaind.c b/onchaind/onchaind.c index 76ca08683dda..84e8d0a7988e 100644 --- a/onchaind/onchaind.c +++ b/onchaind/onchaind.c @@ -1586,13 +1586,28 @@ static void handle_onchaind_spent(struct tracked_output ***outs, const u8 *msg) { struct tx_parts *tx_parts; u32 input_num, tx_blockheight; + struct bitcoin_outpoint *outpoints_to_watch; + size_t num_tracked_before; if (!fromwire_onchaind_spent(msg, msg, &tx_parts, &input_num, &tx_blockheight)) master_badmsg(WIRE_ONCHAIND_SPENT, msg); - /* bwatch (un)watches outputs for us; we no longer report back. */ + /* Any new entries appended to *outs by output_spent are outputs of the + * spending tx that need further resolution; ask lightningd (via bwatch) + * to start watching them. */ + num_tracked_before = tal_count(*outs); output_spent(outs, tx_parts, input_num, tx_blockheight); + + outpoints_to_watch = tal_arr(tmpctx, struct bitcoin_outpoint, 0); + for (size_t i = num_tracked_before; i < tal_count(*outs); i++) + tal_arr_expand(&outpoints_to_watch, (*outs)[i]->outpoint); + + wire_sync_write(REQ_FD, + take(towire_onchaind_watch_outpoints(NULL, + &tx_parts->txid, + tx_blockheight, + outpoints_to_watch))); } static void handle_onchaind_known_preimage(struct tracked_output ***outs, @@ -1668,6 +1683,7 @@ static void wait_for_resolved(struct tracked_output **outs) case WIRE_ONCHAIND_SPEND_HTLC_TIMEOUT: case WIRE_ONCHAIND_SPEND_FULFILL: case WIRE_ONCHAIND_SPEND_HTLC_EXPIRED: + case WIRE_ONCHAIND_WATCH_OUTPOINTS: break; } master_badmsg(-1, msg); diff --git a/onchaind/onchaind_wire.csv b/onchaind/onchaind_wire.csv index e60458320464..4b4c14ac0a95 100644 --- a/onchaind/onchaind_wire.csv +++ b/onchaind/onchaind_wire.csv @@ -67,6 +67,14 @@ msgdata,onchaind_spent,tx,tx_parts, msgdata,onchaind_spent,input_num,u32, msgdata,onchaind_spent,blockheight,u32, +# onchaind->master: register bwatch outpoint watches for outputs of a spending +# tx that onchaind decided are worth tracking (HTLC second-stage, sweeps, ...). +msgtype,onchaind_watch_outpoints,5104 +msgdata,onchaind_watch_outpoints,txid,bitcoin_txid, +msgdata,onchaind_watch_outpoints,blockheight,u32, +msgdata,onchaind_watch_outpoints,num_outpoints,u16, +msgdata,onchaind_watch_outpoints,outpoints,bitcoin_outpoint,num_outpoints + # master->onchaind: We will receive more than one of these, as depth changes. msgtype,onchaind_depth,5005 msgdata,onchaind_depth,txid,bitcoin_txid, diff --git a/onchaind/test/run-grind_feerate.c b/onchaind/test/run-grind_feerate.c index e77543844c6f..5f755c3494e3 100644 --- a/onchaind/test/run-grind_feerate.c +++ b/onchaind/test/run-grind_feerate.c @@ -96,6 +96,9 @@ u8 *towire_onchaind_spend_penalty(const tal_t *ctx UNNEEDED, const struct bitcoi /* Generated stub for towire_onchaind_spend_to_us */ u8 *towire_onchaind_spend_to_us(const tal_t *ctx UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED, struct amount_sat outpoint_amount UNNEEDED, u32 sequence UNNEEDED, u32 minblock UNNEEDED, u64 commit_num UNNEEDED, const u8 *wscript UNNEEDED) { fprintf(stderr, "towire_onchaind_spend_to_us called!\n"); abort(); } +/* Generated stub for towire_onchaind_watch_outpoints */ +u8 *towire_onchaind_watch_outpoints(const tal_t *ctx UNNEEDED, const struct bitcoin_txid *txid UNNEEDED, u32 blockheight UNNEEDED, const struct bitcoin_outpoint *outpoints UNNEEDED) +{ fprintf(stderr, "towire_onchaind_watch_outpoints called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ int main(int argc, char *argv[]) From 4b92b38d90a0baad41531e8efec61641fd0ffe3b Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 11:20:35 +0930 Subject: [PATCH 68/77] lightningd: tear down onchaind bwatch state on irrevocable resolution handle_irrevocably_resolved was leaving the channel_close restart marker and per-tx outpoint watches behind in bwatch, so a re-launched onchaind could be resurrected for a channel we already forgot. --- lightningd/onchain_control.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c index c9b59be42305..11965f8ff4a9 100644 --- a/lightningd/onchain_control.c +++ b/lightningd/onchain_control.c @@ -617,6 +617,11 @@ static void handle_onchain_htlc_timeout(struct channel *channel, const u8 *msg) static void handle_irrevocably_resolved(struct channel *channel, const u8 *msg UNUSED) { + /* Tear down the channel_close restart marker and every per-tx + * bwatch entry; onchaind is done, so there is nothing left to + * resume. */ + onchaind_clear_watches(channel); + /* FIXME: Implement check_htlcs to ensure no dangling hout->in ptrs! */ free_htlcs(channel->peer->ld, channel); From 8257420b8b9eb1ae842afd7ad9f57d2ba3882e90 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 13:03:08 +0930 Subject: [PATCH 69/77] lightningd: extract broadcast.{c,h} from chaintopology Pull the outgoing-tx rebroadcast machinery out of chaintopology into a standalone module. We are going to be removing chaintopology.c in subsequent commits. --- lightningd/Makefile | 1 + lightningd/anchorspend.c | 2 +- lightningd/broadcast.c | 206 ++++++++++++++++++++ lightningd/broadcast.h | 96 +++++++++ lightningd/chaintopology.c | 194 +----------------- lightningd/chaintopology.h | 78 +------- lightningd/onchain_control.c | 6 +- lightningd/peer_control.c | 2 +- lightningd/test/run-invoice-select-inchan.c | 2 +- wallet/test/run-wallet.c | 2 +- 10 files changed, 314 insertions(+), 275 deletions(-) create mode 100644 lightningd/broadcast.c create mode 100644 lightningd/broadcast.h diff --git a/lightningd/Makefile b/lightningd/Makefile index 2d6950d39d7d..ac272645f19e 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -3,6 +3,7 @@ LIGHTNINGD_SRC := \ lightningd/anchorspend.c \ lightningd/bitcoind.c \ + lightningd/broadcast.c \ lightningd/chaintopology.c \ lightningd/watchman.c \ lightningd/channel.c \ diff --git a/lightningd/anchorspend.c b/lightningd/anchorspend.c index 89cf224a4a9d..66a68b37e47b 100644 --- a/lightningd/anchorspend.c +++ b/lightningd/anchorspend.c @@ -602,7 +602,7 @@ static void create_and_broadcast_anchor(struct channel *channel, fmt_amount_sat(tmpctx, anch->anchor_spend_fee)); /* Send it! */ - broadcast_tx(anch->adet, ld->topology, channel, take(newtx), NULL, true, 0, NULL, + broadcast_tx(anch->adet, ld, channel, take(newtx), NULL, true, 0, NULL, refresh_anchor_spend, anch); } diff --git a/lightningd/broadcast.c b/lightningd/broadcast.c new file mode 100644 index 000000000000..2429f197e62c --- /dev/null +++ b/lightningd/broadcast.c @@ -0,0 +1,206 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +bool we_broadcast(const struct lightningd *ld, + const struct bitcoin_txid *txid) +{ + return outgoing_tx_map_exists(ld->topology->outgoing_txs, txid); +} + +struct tx_rebroadcast { + /* otx destructor sets this to NULL if it's been freed */ + struct outgoing_tx *otx; + + /* Pointer to how many are remaining: last one frees! */ + size_t *num_rebroadcast_remaining; +}; + +/* We are last. Refresh timer, and free refcnt */ +static void rebroadcasts_complete(struct lightningd *ld, + size_t *num_rebroadcast_remaining) +{ + tal_free(num_rebroadcast_remaining); + ld->topology->rebroadcast_timer + = new_reltimer(ld->timers, ld->topology, + time_from_sec(30 + pseudorand(30)), + rebroadcast_txs, ld); +} + +static void destroy_tx_broadcast(struct tx_rebroadcast *txrb, + struct lightningd *ld) +{ + if (--*txrb->num_rebroadcast_remaining == 0) + rebroadcasts_complete(ld, txrb->num_rebroadcast_remaining); +} + +static void rebroadcast_done(struct bitcoind *bitcoind, + bool success, const char *msg, + struct tx_rebroadcast *txrb) +{ + if (!success) + log_debug(bitcoind->log, + "Expected error broadcasting tx %s: %s", + fmt_bitcoin_tx(tmpctx, txrb->otx->tx), msg); + + /* Last one freed calls rebroadcasts_complete */ + tal_free(txrb); +} + +/* FIXME: This is dumb. We can group txs and avoid bothering bitcoind + * if any one tx is in the main chain. */ +void rebroadcast_txs(struct lightningd *ld) +{ + /* Copy txs now (peers may go away, and they own txs). */ + struct outgoing_tx *otx; + struct outgoing_tx_map_iter it; + tal_t *cleanup_ctx = tal(NULL, char); + size_t *num_rebroadcast_remaining = notleak(tal(ld, size_t)); + + *num_rebroadcast_remaining = 0; + for (otx = outgoing_tx_map_first(ld->topology->outgoing_txs, &it); otx; + otx = outgoing_tx_map_next(ld->topology->outgoing_txs, &it)) { + struct tx_rebroadcast *txrb; + /* Already sent? */ + if (wallet_transaction_height(ld->wallet, &otx->txid)) + continue; + + /* Don't send ones which aren't ready yet. Note that if the + * minimum block is N, we broadcast it when we have block N-1! */ + if (get_block_height(ld->topology) + 1 < otx->minblock) + continue; + + /* Don't free from txmap inside loop! */ + if (otx->refresh + && !otx->refresh(otx->channel, &otx->tx, otx->cbarg)) { + tal_steal(cleanup_ctx, otx); + continue; + } + + txrb = tal(otx, struct tx_rebroadcast); + txrb->otx = otx; + txrb->num_rebroadcast_remaining = num_rebroadcast_remaining; + (*num_rebroadcast_remaining)++; + tal_add_destructor2(txrb, destroy_tx_broadcast, ld); + bitcoind_sendrawtx(txrb, ld->topology->bitcoind, + tal_strdup_or_null(tmpctx, otx->cmd_id), + fmt_bitcoin_tx(tmpctx, otx->tx), + otx->allowhighfees, + rebroadcast_done, + txrb); + } + tal_free(cleanup_ctx); + + /* Free explicitly in case we were called because a block came in. */ + ld->topology->rebroadcast_timer + = tal_free(ld->topology->rebroadcast_timer); + + /* Nothing to broadcast? Reset timer immediately */ + if (*num_rebroadcast_remaining == 0) + rebroadcasts_complete(ld, num_rebroadcast_remaining); +} + +static void destroy_outgoing_tx(struct outgoing_tx *otx, struct lightningd *ld) +{ + outgoing_tx_map_del(ld->topology->outgoing_txs, otx); +} + +static void broadcast_done(struct bitcoind *bitcoind, + bool success, const char *msg, + struct outgoing_tx *otx) +{ + struct lightningd *ld = bitcoind->ld; + + if (otx->finished) { + if (otx->finished(otx->channel, otx->tx, success, msg, otx->cbarg)) { + tal_free(otx); + return; + } + } + + if (we_broadcast(ld, &otx->txid)) { + log_debug(ld->log, + "Not adding %s to list of outgoing transactions, already " + "present", + fmt_bitcoin_txid(tmpctx, &otx->txid)); + tal_free(otx); + return; + } + + /* For continual rebroadcasting, until context freed. */ + outgoing_tx_map_add(ld->topology->outgoing_txs, otx); + tal_add_destructor2(otx, destroy_outgoing_tx, ld); +} + +void broadcast_tx_(const tal_t *ctx, + struct lightningd *ld, + struct channel *channel, const struct bitcoin_tx *tx, + const char *cmd_id, bool allowhighfees, u32 minblock, + bool (*finished)(struct channel *channel, + const struct bitcoin_tx *tx, + bool success, + const char *err, + void *cbarg), + bool (*refresh)(struct channel *channel, + const struct bitcoin_tx **tx, + void *cbarg), + void *cbarg) +{ + struct outgoing_tx *otx = tal(ctx, struct outgoing_tx); + + otx->channel = channel; + bitcoin_txid(tx, &otx->txid); + otx->tx = clone_bitcoin_tx(otx, tx); + otx->minblock = minblock; + otx->allowhighfees = allowhighfees; + otx->finished = finished; + otx->refresh = refresh; + otx->cbarg = cbarg; + if (taken(otx->cbarg)) + tal_steal(otx, otx->cbarg); + otx->cmd_id = tal_strdup_or_null(otx, cmd_id); + + /* Note that if the minimum block is N, we broadcast it when + * we have block N-1! */ + if (get_block_height(ld->topology) + 1 < otx->minblock) { + log_debug(ld->log, + "Deferring broadcast of txid %s until block %u", + fmt_bitcoin_txid(tmpctx, &otx->txid), + otx->minblock - 1); + + /* For continual rebroadcasting, until channel freed. */ + tal_steal(otx->channel, otx); + outgoing_tx_map_add(ld->topology->outgoing_txs, otx); + tal_add_destructor2(otx, destroy_outgoing_tx, ld); + return; + } + + log_debug(ld->log, "Broadcasting txid %s%s%s", + fmt_bitcoin_txid(tmpctx, &otx->txid), + cmd_id ? " for " : "", cmd_id ? cmd_id : ""); + + wallet_transaction_add(ld->wallet, tx->wtx, 0, 0); + bitcoind_sendrawtx(otx, ld->topology->bitcoind, otx->cmd_id, + fmt_bitcoin_tx(tmpctx, otx->tx), + allowhighfees, + broadcast_done, otx); +} + +void broadcast_shutdown(struct lightningd *ld) +{ + struct outgoing_tx *otx; + struct outgoing_tx_map_iter it; + for (otx = outgoing_tx_map_first(ld->topology->outgoing_txs, &it); otx; + otx = outgoing_tx_map_next(ld->topology->outgoing_txs, &it)) { + tal_del_destructor2(otx, destroy_outgoing_tx, ld); + tal_free(otx); + } +} diff --git a/lightningd/broadcast.h b/lightningd/broadcast.h new file mode 100644 index 000000000000..187fd36b4684 --- /dev/null +++ b/lightningd/broadcast.h @@ -0,0 +1,96 @@ +#ifndef LIGHTNING_LIGHTNINGD_BROADCAST_H +#define LIGHTNING_LIGHTNINGD_BROADCAST_H +#include "config.h" +#include +#include + +struct lightningd; + +/* Off ld->outgoing_txs */ +struct outgoing_tx { + struct channel *channel; + const struct bitcoin_tx *tx; + struct bitcoin_txid txid; + u32 minblock; + bool allowhighfees; + const char *cmd_id; + bool (*finished)(struct channel *channel, const struct bitcoin_tx *, + bool success, const char *err, void *arg); + bool (*refresh)(struct channel *, const struct bitcoin_tx **, void *arg); + void *cbarg; +}; + +static inline const struct bitcoin_txid *keyof_outgoing_tx_map(const struct outgoing_tx *t) +{ + return &t->txid; +} + +static inline size_t outgoing_tx_hash_sha(const struct bitcoin_txid *key) +{ + size_t ret; + memcpy(&ret, key, sizeof(ret)); + return ret; +} + +static inline bool outgoing_tx_eq(const struct outgoing_tx *b, const struct bitcoin_txid *key) +{ + return bitcoin_txid_eq(&b->txid, key); +} +HTABLE_DEFINE_DUPS_TYPE(struct outgoing_tx, keyof_outgoing_tx_map, + outgoing_tx_hash_sha, outgoing_tx_eq, + outgoing_tx_map); + +/** + * broadcast_tx - Broadcast a single tx, and rebroadcast as reqd (copies tx). + * @ctx: context: when this is freed, callback/retransmission don't happen. + * @ld: lightningd + * @channel: the channel responsible for this (stop broadcasting if freed). + * @tx: the transaction + * @cmd_id: the JSON command id which triggered this (or NULL). + * @allowhighfees: set to true to override the high-fee checks in the backend. + * @minblock: minimum block we can send it at (or 0). + * @finished: if non-NULL, call every time sendrawtransaction returns; if it returns true, don't rebroadcast. + * @refresh: if non-NULL, callback before re-broadcasting (can replace tx): + * if returns false, delete. + * @cbarg: argument for @finished and @refresh + */ +#define broadcast_tx(ctx, ld, channel, tx, cmd_id, allowhighfees, \ + minblock, finished, refresh, cbarg) \ + broadcast_tx_((ctx), (ld), (channel), (tx), (cmd_id), (allowhighfees), \ + (minblock), \ + typesafe_cb_preargs(bool, void *, \ + (finished), (cbarg), \ + struct channel *, \ + const struct bitcoin_tx *, \ + bool, const char *), \ + typesafe_cb_preargs(bool, void *, \ + (refresh), (cbarg), \ + struct channel *, \ + const struct bitcoin_tx **), \ + (cbarg)) + +void broadcast_tx_(const tal_t *ctx, + struct lightningd *ld, + struct channel *channel, + const struct bitcoin_tx *tx TAKES, + const char *cmd_id, bool allowhighfees, u32 minblock, + bool (*finished)(struct channel *, + const struct bitcoin_tx *, + bool success, + const char *err, + void *), + bool (*refresh)(struct channel *, const struct bitcoin_tx **, void *), + void *cbarg TAKES); + +/* Rebroadcast unconfirmed txs. Called when a new block is processed. */ +void rebroadcast_txs(struct lightningd *ld); + +/* True iff there's a pending outgoing tx with this txid. */ +bool we_broadcast(const struct lightningd *ld, + const struct bitcoin_txid *txid); + +/* Drain all pending outgoing txs at shutdown, before channels (and their + * outgoing_tx destructors) are freed. */ +void broadcast_shutdown(struct lightningd *ld); + +#endif /* LIGHTNING_LIGHTNINGD_BROADCAST_H */ diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index bc5248b882bb..c6adcb0384d4 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -32,12 +32,6 @@ static void next_topology_timer(struct chain_topology *topo) try_extend_tip, topo); } -static bool we_broadcast(const struct chain_topology *topo, - const struct bitcoin_txid *txid) -{ - return outgoing_tx_map_exists(topo->outgoing_txs, txid); -} - static void filter_block_txs(struct chain_topology *topo, struct block *b) { /* Now we see if any of those txs are interesting. */ @@ -91,7 +85,7 @@ static void filter_block_txs(struct chain_topology *topo, struct block *b) /* Make sure we preserve any transaction we are interested in */ if (watch_check_tx_outputs(topo, &loc, tx, &txid) || watching_txid(topo, &txid) - || we_broadcast(topo, &txid)) { + || we_broadcast(topo->ld, &txid)) { wallet_transaction_add(topo->ld->wallet, tx->wtx, b->height, i); } @@ -112,182 +106,6 @@ size_t get_tx_depth(const struct chain_topology *topo, return topo->tip->height - blockheight + 1; } -struct tx_rebroadcast { - /* otx destructor sets this to NULL if it's been freed */ - struct outgoing_tx *otx; - - /* Pointer to how many are remaining: last one frees! */ - size_t *num_rebroadcast_remaining; -}; - -/* Timer recursion: declare now. */ -static void rebroadcast_txs(struct chain_topology *topo); - -/* We are last. Refresh timer, and free refcnt */ -static void rebroadcasts_complete(struct chain_topology *topo, - size_t *num_rebroadcast_remaining) -{ - tal_free(num_rebroadcast_remaining); - topo->rebroadcast_timer = new_reltimer(topo->ld->timers, topo, - time_from_sec(30 + pseudorand(30)), - rebroadcast_txs, topo); -} - -static void destroy_tx_broadcast(struct tx_rebroadcast *txrb, struct chain_topology *topo) -{ - if (--*txrb->num_rebroadcast_remaining == 0) - rebroadcasts_complete(topo, txrb->num_rebroadcast_remaining); -} - -static void rebroadcast_done(struct bitcoind *bitcoind, - bool success, const char *msg, - struct tx_rebroadcast *txrb) -{ - if (!success) - log_debug(bitcoind->log, - "Expected error broadcasting tx %s: %s", - fmt_bitcoin_tx(tmpctx, txrb->otx->tx), msg); - - /* Last one freed calls rebroadcasts_complete */ - tal_free(txrb); -} - -/* FIXME: This is dumb. We can group txs and avoid bothering bitcoind - * if any one tx is in the main chain. */ -static void rebroadcast_txs(struct chain_topology *topo) -{ - /* Copy txs now (peers may go away, and they own txs). */ - struct outgoing_tx *otx; - struct outgoing_tx_map_iter it; - tal_t *cleanup_ctx = tal(NULL, char); - size_t *num_rebroadcast_remaining = notleak(tal(topo, size_t)); - - *num_rebroadcast_remaining = 0; - for (otx = outgoing_tx_map_first(topo->outgoing_txs, &it); otx; - otx = outgoing_tx_map_next(topo->outgoing_txs, &it)) { - struct tx_rebroadcast *txrb; - /* Already sent? */ - if (wallet_transaction_height(topo->ld->wallet, &otx->txid)) - continue; - - /* Don't send ones which aren't ready yet. Note that if the - * minimum block is N, we broadcast it when we have block N-1! */ - if (get_block_height(topo) + 1 < otx->minblock) - continue; - - /* Don't free from txmap inside loop! */ - if (otx->refresh - && !otx->refresh(otx->channel, &otx->tx, otx->cbarg)) { - tal_steal(cleanup_ctx, otx); - continue; - } - - txrb = tal(otx, struct tx_rebroadcast); - txrb->otx = otx; - txrb->num_rebroadcast_remaining = num_rebroadcast_remaining; - (*num_rebroadcast_remaining)++; - tal_add_destructor2(txrb, destroy_tx_broadcast, topo); - bitcoind_sendrawtx(txrb, topo->bitcoind, - tal_strdup_or_null(tmpctx, otx->cmd_id), - fmt_bitcoin_tx(tmpctx, otx->tx), - otx->allowhighfees, - rebroadcast_done, - txrb); - } - tal_free(cleanup_ctx); - - /* Free explicitly in case we were called because a block came in. */ - topo->rebroadcast_timer = tal_free(topo->rebroadcast_timer); - - /* Nothing to broadcast? Reset timer immediately */ - if (*num_rebroadcast_remaining == 0) - rebroadcasts_complete(topo, num_rebroadcast_remaining); -} - -static void destroy_outgoing_tx(struct outgoing_tx *otx, struct chain_topology *topo) -{ - outgoing_tx_map_del(topo->outgoing_txs, otx); -} - -static void broadcast_done(struct bitcoind *bitcoind, - bool success, const char *msg, - struct outgoing_tx *otx) -{ - if (otx->finished) { - if (otx->finished(otx->channel, otx->tx, success, msg, otx->cbarg)) { - tal_free(otx); - return; - } - } - - if (we_broadcast(bitcoind->ld->topology, &otx->txid)) { - log_debug( - bitcoind->ld->topology->log, - "Not adding %s to list of outgoing transactions, already " - "present", - fmt_bitcoin_txid(tmpctx, &otx->txid)); - tal_free(otx); - return; - } - - /* For continual rebroadcasting, until context freed. */ - outgoing_tx_map_add(bitcoind->ld->topology->outgoing_txs, otx); - tal_add_destructor2(otx, destroy_outgoing_tx, bitcoind->ld->topology); -} - -void broadcast_tx_(const tal_t *ctx, - struct chain_topology *topo, - struct channel *channel, const struct bitcoin_tx *tx, - const char *cmd_id, bool allowhighfees, u32 minblock, - bool (*finished)(struct channel *channel, - const struct bitcoin_tx *tx, - bool success, - const char *err, - void *cbarg), - bool (*refresh)(struct channel *channel, - const struct bitcoin_tx **tx, - void *cbarg), - void *cbarg) -{ - struct outgoing_tx *otx = tal(ctx, struct outgoing_tx); - - otx->channel = channel; - bitcoin_txid(tx, &otx->txid); - otx->tx = clone_bitcoin_tx(otx, tx); - otx->minblock = minblock; - otx->allowhighfees = allowhighfees; - otx->finished = finished; - otx->refresh = refresh; - otx->cbarg = cbarg; - if (taken(otx->cbarg)) - tal_steal(otx, otx->cbarg); - otx->cmd_id = tal_strdup_or_null(otx, cmd_id); - - /* Note that if the minimum block is N, we broadcast it when - * we have block N-1! */ - if (get_block_height(topo) + 1 < otx->minblock) { - log_debug(topo->log, "Deferring broadcast of txid %s until block %u", - fmt_bitcoin_txid(tmpctx, &otx->txid), - otx->minblock - 1); - - /* For continual rebroadcasting, until channel freed. */ - tal_steal(otx->channel, otx); - outgoing_tx_map_add(topo->outgoing_txs, otx); - tal_add_destructor2(otx, destroy_outgoing_tx, topo); - return; - } - - log_debug(topo->log, "Broadcasting txid %s%s%s", - fmt_bitcoin_txid(tmpctx, &otx->txid), - cmd_id ? " for " : "", cmd_id ? cmd_id : ""); - - wallet_transaction_add(topo->ld->wallet, tx->wtx, 0, 0); - bitcoind_sendrawtx(otx, topo->bitcoind, otx->cmd_id, - fmt_bitcoin_tx(tmpctx, otx->tx), - allowhighfees, - broadcast_done, otx); -} - static enum watch_result closeinfo_txid_confirmed(struct lightningd *ld, const struct bitcoin_txid *txid, const struct bitcoin_tx *tx, @@ -867,7 +685,7 @@ static void updates_complete(struct chain_topology *topo) watch_topology_changed(topo); /* Maybe need to rebroadcast. */ - rebroadcast_txs(topo); + rebroadcast_txs(topo->ld); /* We've processed these UTXOs */ db_set_intvar(topo->bitcoind->ld->wallet->db, @@ -1228,13 +1046,7 @@ u32 default_locktime(const struct chain_topology *topo) * do it now instead. */ static void destroy_chain_topology(struct chain_topology *topo) { - struct outgoing_tx *otx; - struct outgoing_tx_map_iter it; - for (otx = outgoing_tx_map_first(topo->outgoing_txs, &it); otx; - otx = outgoing_tx_map_next(topo->outgoing_txs, &it)) { - tal_del_destructor2(otx, destroy_outgoing_tx, topo); - tal_free(otx); - } + broadcast_shutdown(topo->ld); } struct chain_topology *new_topology(struct lightningd *ld, struct logger *log) diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index 9a69f34c82ea..9139d1d3b06f 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -1,6 +1,7 @@ #ifndef LIGHTNING_LIGHTNINGD_CHAINTOPOLOGY_H #define LIGHTNING_LIGHTNINGD_CHAINTOPOLOGY_H #include "config.h" +#include #include struct bitcoin_tx; @@ -15,20 +16,6 @@ struct wallet; /* We keep the last three in case there are outliers (for min/max) */ #define FEE_HISTORY_NUM 3 -/* Off topology->outgoing_txs */ -struct outgoing_tx { - struct channel *channel; - const struct bitcoin_tx *tx; - struct bitcoin_txid txid; - u32 minblock; - bool allowhighfees; - const char *cmd_id; - bool (*finished)(struct channel *channel, const struct bitcoin_tx *, - bool success, const char *err, void *arg); - bool (*refresh)(struct channel *, const struct bitcoin_tx **, void *arg); - void *cbarg; -}; - struct block { u32 height; @@ -70,27 +57,6 @@ static inline bool block_eq(const struct block *b, const struct bitcoin_blkid *k HTABLE_DEFINE_NODUPS_TYPE(struct block, keyof_block_map, hash_sha, block_eq, block_map); -/* Hash blocks by sha */ -static inline const struct bitcoin_txid *keyof_outgoing_tx_map(const struct outgoing_tx *t) -{ - return &t->txid; -} - -static inline size_t outgoing_tx_hash_sha(const struct bitcoin_txid *key) -{ - size_t ret; - memcpy(&ret, key, sizeof(ret)); - return ret; -} - -static inline bool outgoing_tx_eq(const struct outgoing_tx *b, const struct bitcoin_txid *key) -{ - return bitcoin_txid_eq(&b->txid, key); -} -HTABLE_DEFINE_DUPS_TYPE(struct outgoing_tx, keyof_outgoing_tx_map, - outgoing_tx_hash_sha, outgoing_tx_eq, - outgoing_tx_map); - /* Our plugins give us a series of blockcount, feerate pairs. */ struct feerate_est { u32 blockcount; @@ -206,48 +172,6 @@ u32 penalty_feerate(struct chain_topology *topo); /* Usually we set nLocktime to tip (or recent) like bitcoind does */ u32 default_locktime(const struct chain_topology *topo); -/** - * broadcast_tx - Broadcast a single tx, and rebroadcast as reqd (copies tx). - * @ctx: context: when this is freed, callback/retransmission don't happen. - * @topo: topology - * @channel: the channel responsible for this (stop broadcasting if freed). - * @tx: the transaction - * @cmd_id: the JSON command id which triggered this (or NULL). - * @allowhighfees: set to true to override the high-fee checks in the backend. - * @minblock: minimum block we can send it at (or 0). - * @finished: if non-NULL, call every time sendrawtransaction returns; if it returns true, don't rebroadcast. - * @refresh: if non-NULL, callback before re-broadcasting (can replace tx): - * if returns false, delete. - * @cbarg: argument for @finished and @refresh - */ -#define broadcast_tx(ctx, topo, channel, tx, cmd_id, allowhighfees, \ - minblock, finished, refresh, cbarg) \ - broadcast_tx_((ctx), (topo), (channel), (tx), (cmd_id), (allowhighfees), \ - (minblock), \ - typesafe_cb_preargs(bool, void *, \ - (finished), (cbarg), \ - struct channel *, \ - const struct bitcoin_tx *, \ - bool, const char *), \ - typesafe_cb_preargs(bool, void *, \ - (refresh), (cbarg), \ - struct channel *, \ - const struct bitcoin_tx **), \ - (cbarg)) - -void broadcast_tx_(const tal_t *ctx, - struct chain_topology *topo, - struct channel *channel, - const struct bitcoin_tx *tx TAKES, - const char *cmd_id, bool allowhighfees, u32 minblock, - bool (*finished)(struct channel *, - const struct bitcoin_tx *, - bool success, - const char *err, - void *), - bool (*refresh)(struct channel *, const struct bitcoin_tx **, void *), - void *cbarg TAKES); - struct chain_topology *new_topology(struct lightningd *ld, struct logger *log); void setup_topology(struct chain_topology *topology); diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c index 11965f8ff4a9..21486932ff1f 100644 --- a/lightningd/onchain_control.c +++ b/lightningd/onchain_control.c @@ -1380,7 +1380,7 @@ static void create_onchain_tx(struct channel *channel, /* We allow "excessive" fees, as we may be fighting with censors and * we'd rather spend fees than have our adversary win. */ - broadcast_tx(channel, ld->topology, + broadcast_tx(channel, ld, channel, take(tx), NULL, true, info->minblock, NULL, consider_onchain_rebroadcast, take(info)); @@ -1586,7 +1586,7 @@ static void handle_onchaind_spend_htlc_success(struct channel *channel, log_debug(channel->log, "Broadcast for onchaind tx %s", fmt_bitcoin_tx(tmpctx, tx)); - broadcast_tx(channel, channel->peer->ld->topology, + broadcast_tx(channel, channel->peer->ld, channel, take(tx), NULL, false, info->minblock, NULL, consider_onchain_htlc_tx_rebroadcast, take(info)); @@ -1668,7 +1668,7 @@ static void handle_onchaind_spend_htlc_timeout(struct channel *channel, log_debug(channel->log, "Broadcast for onchaind tx %s", fmt_bitcoin_tx(tmpctx, tx)); - broadcast_tx(channel, channel->peer->ld->topology, + broadcast_tx(channel, channel->peer->ld, channel, take(tx), NULL, false, info->minblock, NULL, consider_onchain_htlc_tx_rebroadcast, take(info)); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 72e9d3bd52e2..e5d5f5806e2b 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -318,7 +318,7 @@ static struct bitcoin_tx *sign_and_send_last(const tal_t *ctx, /* Keep broadcasting until we say stop (can fail due to dup, * if they beat us to the broadcast). */ - broadcast_tx(channel, ld->topology, channel, tx, cmd_id, false, 0, + broadcast_tx(channel, ld, channel, tx, cmd_id, false, 0, commit_tx_send_finished, NULL, take(adet)); return tx; diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index 8ed2322851ca..7a615ee0f819 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -41,7 +41,7 @@ void bitcoind_sendrawtx_(const tal_t *ctx UNNEEDED, { fprintf(stderr, "bitcoind_sendrawtx_ called!\n"); abort(); } /* Generated stub for broadcast_tx_ */ void broadcast_tx_(const tal_t *ctx UNNEEDED, - struct chain_topology *topo UNNEEDED, + struct lightningd *ld UNNEEDED, struct channel *channel UNNEEDED, const struct bitcoin_tx *tx TAKES UNNEEDED, const char *cmd_id UNNEEDED, bool allowhighfees UNNEEDED, u32 minblock UNNEEDED, diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 0cd9dfa73bbf..8574da97b3ba 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -81,7 +81,7 @@ void bitcoind_sendrawtx_(const tal_t *ctx UNNEEDED, { fprintf(stderr, "bitcoind_sendrawtx_ called!\n"); abort(); } /* Generated stub for broadcast_tx_ */ void broadcast_tx_(const tal_t *ctx UNNEEDED, - struct chain_topology *topo UNNEEDED, + struct lightningd *ld UNNEEDED, struct channel *channel UNNEEDED, const struct bitcoin_tx *tx TAKES UNNEEDED, const char *cmd_id UNNEEDED, bool allowhighfees UNNEEDED, u32 minblock UNNEEDED, From ab4ee1fb42cd6590543e9c936722015bac345f64 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 13:20:57 +0930 Subject: [PATCH 70/77] lightningd: move outgoing_txs and rebroadcast_timer onto struct lightningd First step in dismantling chain_topology: these two fields are owned by broadcast.c, so they belong on lightningd. --- lightningd/broadcast.c | 26 ++++++++++++-------------- lightningd/chaintopology.c | 2 -- lightningd/chaintopology.h | 5 +---- lightningd/lightningd.c | 2 ++ lightningd/lightningd.h | 6 ++++++ 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lightningd/broadcast.c b/lightningd/broadcast.c index 2429f197e62c..4c34bef78e46 100644 --- a/lightningd/broadcast.c +++ b/lightningd/broadcast.c @@ -13,7 +13,7 @@ bool we_broadcast(const struct lightningd *ld, const struct bitcoin_txid *txid) { - return outgoing_tx_map_exists(ld->topology->outgoing_txs, txid); + return outgoing_tx_map_exists(ld->outgoing_txs, txid); } struct tx_rebroadcast { @@ -29,10 +29,9 @@ static void rebroadcasts_complete(struct lightningd *ld, size_t *num_rebroadcast_remaining) { tal_free(num_rebroadcast_remaining); - ld->topology->rebroadcast_timer - = new_reltimer(ld->timers, ld->topology, - time_from_sec(30 + pseudorand(30)), - rebroadcast_txs, ld); + ld->rebroadcast_timer = new_reltimer(ld->timers, ld, + time_from_sec(30 + pseudorand(30)), + rebroadcast_txs, ld); } static void destroy_tx_broadcast(struct tx_rebroadcast *txrb, @@ -66,8 +65,8 @@ void rebroadcast_txs(struct lightningd *ld) size_t *num_rebroadcast_remaining = notleak(tal(ld, size_t)); *num_rebroadcast_remaining = 0; - for (otx = outgoing_tx_map_first(ld->topology->outgoing_txs, &it); otx; - otx = outgoing_tx_map_next(ld->topology->outgoing_txs, &it)) { + for (otx = outgoing_tx_map_first(ld->outgoing_txs, &it); otx; + otx = outgoing_tx_map_next(ld->outgoing_txs, &it)) { struct tx_rebroadcast *txrb; /* Already sent? */ if (wallet_transaction_height(ld->wallet, &otx->txid)) @@ -100,8 +99,7 @@ void rebroadcast_txs(struct lightningd *ld) tal_free(cleanup_ctx); /* Free explicitly in case we were called because a block came in. */ - ld->topology->rebroadcast_timer - = tal_free(ld->topology->rebroadcast_timer); + ld->rebroadcast_timer = tal_free(ld->rebroadcast_timer); /* Nothing to broadcast? Reset timer immediately */ if (*num_rebroadcast_remaining == 0) @@ -110,7 +108,7 @@ void rebroadcast_txs(struct lightningd *ld) static void destroy_outgoing_tx(struct outgoing_tx *otx, struct lightningd *ld) { - outgoing_tx_map_del(ld->topology->outgoing_txs, otx); + outgoing_tx_map_del(ld->outgoing_txs, otx); } static void broadcast_done(struct bitcoind *bitcoind, @@ -136,7 +134,7 @@ static void broadcast_done(struct bitcoind *bitcoind, } /* For continual rebroadcasting, until context freed. */ - outgoing_tx_map_add(ld->topology->outgoing_txs, otx); + outgoing_tx_map_add(ld->outgoing_txs, otx); tal_add_destructor2(otx, destroy_outgoing_tx, ld); } @@ -178,7 +176,7 @@ void broadcast_tx_(const tal_t *ctx, /* For continual rebroadcasting, until channel freed. */ tal_steal(otx->channel, otx); - outgoing_tx_map_add(ld->topology->outgoing_txs, otx); + outgoing_tx_map_add(ld->outgoing_txs, otx); tal_add_destructor2(otx, destroy_outgoing_tx, ld); return; } @@ -198,8 +196,8 @@ void broadcast_shutdown(struct lightningd *ld) { struct outgoing_tx *otx; struct outgoing_tx_map_iter it; - for (otx = outgoing_tx_map_first(ld->topology->outgoing_txs, &it); otx; - otx = outgoing_tx_map_next(ld->topology->outgoing_txs, &it)) { + for (otx = outgoing_tx_map_first(ld->outgoing_txs, &it); otx; + otx = outgoing_tx_map_next(ld->outgoing_txs, &it)) { tal_del_destructor2(otx, destroy_outgoing_tx, ld); tal_free(otx); } diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index c6adcb0384d4..0c2d2538531e 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -1055,7 +1055,6 @@ struct chain_topology *new_topology(struct lightningd *ld, struct logger *log) topo->ld = ld; topo->block_map = new_htable(topo, block_map); - topo->outgoing_txs = new_htable(topo, outgoing_tx_map); topo->txwatches = new_htable(topo, txwatch_hash); topo->txowatches = new_htable(topo, txowatch_hash); topo->scriptpubkeywatches = new_htable(topo, scriptpubkeywatch_hash); @@ -1068,7 +1067,6 @@ struct chain_topology *new_topology(struct lightningd *ld, struct logger *log) topo->root = NULL; topo->sync_waiters = tal(topo, struct list_head); topo->extend_timer = NULL; - topo->rebroadcast_timer = NULL; topo->updatefee_timer = NULL; topo->checkchain_timer = NULL; topo->request_ctx = tal(topo, char); diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index 9139d1d3b06f..9371d0f6849a 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -95,14 +95,11 @@ struct chain_topology { struct bitcoind *bitcoind; /* Timers we're running. */ - struct oneshot *checkchain_timer, *extend_timer, *updatefee_timer, *rebroadcast_timer; + struct oneshot *checkchain_timer, *extend_timer, *updatefee_timer; /* Parent context for requests (to bcli plugin) we have outstanding. */ tal_t *request_ctx; - /* Bitcoin transactions we're broadcasting */ - struct outgoing_tx_map *outgoing_txs; - /* Transactions/txos we are watching. */ struct txwatch_hash *txwatches; struct txowatch_hash *txowatches; diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 35ba0679f3ad..fb8619c29bc5 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -274,6 +274,8 @@ static struct lightningd *new_lightningd(const tal_t *ctx) /*~ This is detailed in chaintopology.c */ ld->topology = new_topology(ld, ld->log); + ld->outgoing_txs = new_htable(ld, outgoing_tx_map); + ld->rebroadcast_timer = NULL; ld->gossip_blockheight = 0; ld->daemon_parent_fd = -1; ld->proxyaddr = NULL; diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h index d2a4f23fc74b..ab5fe729695f 100644 --- a/lightningd/lightningd.h +++ b/lightningd/lightningd.h @@ -12,6 +12,8 @@ #include struct amount_msat; +struct oneshot; +struct outgoing_tx_map; struct watchman; /* Various adjustable things. */ @@ -225,6 +227,10 @@ struct lightningd { /* Our chain topology. */ struct chain_topology *topology; + /* Bitcoin transactions we're broadcasting */ + struct outgoing_tx_map *outgoing_txs; + struct oneshot *rebroadcast_timer; + /* Blockheight (as acknowledged by gossipd) */ u32 gossip_blockheight; From dfa08df1e673d1599d39938bf98a43ae9cb22e48 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 13:28:37 +0930 Subject: [PATCH 71/77] lightningd: move bitcoind onto struct lightningd Part of the chaintopology spring clean. --- lightningd/broadcast.c | 4 +-- lightningd/chaintopology.c | 41 +++++++++++++-------------- lightningd/chaintopology.h | 3 -- lightningd/channel_control.c | 8 +++--- lightningd/dual_open_control.c | 8 +++--- lightningd/lightningd.c | 2 ++ lightningd/lightningd.h | 4 +++ lightningd/peer_control.c | 6 ++-- lightningd/test/run-find_my_abspath.c | 5 ++++ lightningd/watch.c | 2 +- lightningd/watchman.c | 10 +++---- wallet/wallet.c | 2 +- wallet/walletrpc.c | 4 +-- 13 files changed, 53 insertions(+), 46 deletions(-) diff --git a/lightningd/broadcast.c b/lightningd/broadcast.c index 4c34bef78e46..1f6621bb264f 100644 --- a/lightningd/broadcast.c +++ b/lightningd/broadcast.c @@ -89,7 +89,7 @@ void rebroadcast_txs(struct lightningd *ld) txrb->num_rebroadcast_remaining = num_rebroadcast_remaining; (*num_rebroadcast_remaining)++; tal_add_destructor2(txrb, destroy_tx_broadcast, ld); - bitcoind_sendrawtx(txrb, ld->topology->bitcoind, + bitcoind_sendrawtx(txrb, ld->bitcoind, tal_strdup_or_null(tmpctx, otx->cmd_id), fmt_bitcoin_tx(tmpctx, otx->tx), otx->allowhighfees, @@ -186,7 +186,7 @@ void broadcast_tx_(const tal_t *ctx, cmd_id ? " for " : "", cmd_id ? cmd_id : ""); wallet_transaction_add(ld->wallet, tx->wtx, 0, 0); - bitcoind_sendrawtx(otx, ld->topology->bitcoind, otx->cmd_id, + bitcoind_sendrawtx(otx, ld->bitcoind, otx->cmd_id, fmt_bitcoin_tx(tmpctx, otx->tx), allowhighfees, broadcast_done, otx); diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index 0c2d2538531e..e00cc83940ac 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -62,7 +62,7 @@ static void filter_block_txs(struct chain_topology *topo, struct block *b) txid = b->txids[i]; our_outnums = tal_arr(tmpctx, size_t, 0); - if (wallet_extract_owned_outputs(topo->bitcoind->ld->wallet, + if (wallet_extract_owned_outputs(topo->ld->wallet, tx->wtx, is_coinbase, &b->height, &our_outnums)) { wallet_transaction_add(topo->ld->wallet, tx->wtx, b->height, i); @@ -411,7 +411,7 @@ static void start_fee_estimate(struct chain_topology *topo) { topo->updatefee_timer = NULL; /* Based on timer, update fee estimates. */ - bitcoind_estimate_fees(topo->request_ctx, topo->bitcoind, update_feerates_repeat, NULL); + bitcoind_estimate_fees(topo->request_ctx, topo->ld->bitcoind, update_feerates_repeat, NULL); } struct rate_conversion { @@ -676,7 +676,7 @@ static void updates_complete(struct chain_topology *topo) { if (!bitcoin_blkid_eq(&topo->tip->blkid, &topo->prev_tip)) { /* Tell lightningd about new block. */ - notify_new_block(topo->bitcoind->ld); + notify_new_block(topo->ld); /* Tell blockdepth watchers */ watch_check_block_added(topo, topo->tip->height); @@ -688,7 +688,7 @@ static void updates_complete(struct chain_topology *topo) rebroadcast_txs(topo->ld); /* We've processed these UTXOs */ - db_set_intvar(topo->bitcoind->ld->wallet->db, + db_set_intvar(topo->ld->wallet->db, "last_processed_block", topo->tip->height); topo->prev_tip = topo->tip->blkid; @@ -701,7 +701,7 @@ static void updates_complete(struct chain_topology *topo) } /* If bitcoind is synced, we're now synced. */ - if (topo->bitcoind->synced && !topology_synced(topo)) { + if (topo->ld->bitcoind->synced && !topology_synced(topo)) { struct sync_waiter *w; struct list_head *list = topo->sync_waiters; @@ -772,7 +772,7 @@ static void topo_update_spends(struct chain_topology *topo, * tell gossipd about them. */ spent_scids = wallet_utxoset_get_spent(tmpctx, topo->ld->wallet, blockheight); - gossipd_notify_spends(topo->bitcoind->ld, blockheight, spent_scids); + gossipd_notify_spends(topo->ld, blockheight, spent_scids); } static void topo_add_utxos(struct chain_topology *topo, struct block *b) @@ -895,7 +895,7 @@ static void remove_tip(struct chain_topology *topo) /* These no longer exist, so gossipd drops any reference to them just * as if they were spent. */ - gossipd_notify_spends(topo->bitcoind->ld, b->height, removed_scids); + gossipd_notify_spends(topo->ld, b->height, removed_scids); tal_free(b); } @@ -936,7 +936,7 @@ static void try_extend_tip(struct chain_topology *topo) { topo->extend_timer = NULL; trace_span_start("extend_tip", topo); - bitcoind_getrawblockbyheight(topo->request_ctx, topo->bitcoind, topo->tip->height + 1, + bitcoind_getrawblockbyheight(topo->request_ctx, topo->ld->bitcoind, topo->tip->height + 1, get_new_block, topo); } @@ -1060,7 +1060,6 @@ struct chain_topology *new_topology(struct lightningd *ld, struct logger *log) topo->scriptpubkeywatches = new_htable(topo, scriptpubkeywatch_hash); topo->blockdepthwatches = new_htable(topo, blockdepthwatch_hash); topo->log = log; - topo->bitcoind = new_bitcoind(topo, ld, log); topo->poll_seconds = 30; memset(topo->feerates, 0, sizeof(topo->feerates)); topo->smoothed_feerates = NULL; @@ -1128,7 +1127,7 @@ static void retry_sync_getchaininfo_done(struct bitcoind *bitcoind, const char * static void retry_sync(struct chain_topology *topo) { topo->checkchain_timer = NULL; - bitcoind_getchaininfo(topo->request_ctx, topo->bitcoind, get_block_height(topo), + bitcoind_getchaininfo(topo->request_ctx, topo->ld->bitcoind, get_block_height(topo), retry_sync_getchaininfo_done, topo); } @@ -1225,7 +1224,7 @@ void setup_topology(struct chain_topology *topo) s64 fixup; /* This waits for bitcoind. */ - bitcoind_check_commands(topo->bitcoind); + bitcoind_check_commands(topo->ld->bitcoind); /* For testing.. */ log_debug(topo->ld->log, "All Bitcoin plugin commands registered"); @@ -1262,9 +1261,9 @@ void setup_topology(struct chain_topology *topo) /* Sanity checks, then topology initialization. */ chaininfo->chain = NULL; feerates->rates = NULL; - bitcoind_getchaininfo(chaininfo, topo->bitcoind, blockscan_start, + bitcoind_getchaininfo(chaininfo, topo->ld->bitcoind, blockscan_start, get_chaininfo_once, chaininfo); - bitcoind_estimate_fees(feerates, topo->bitcoind, get_feerates_once, feerates); + bitcoind_estimate_fees(feerates, topo->ld->bitcoind, get_feerates_once, feerates); /* Each one will break, but they might only exit once! */ ret = io_loop_with_timers(topo->ld); @@ -1291,34 +1290,34 @@ void setup_topology(struct chain_topology *topo) blockscan_start, chaininfo->blockcount); } else if (chaininfo->blockcount < blockscan_start) { struct wait_for_height *wh = tal(local_ctx, struct wait_for_height); - wh->bitcoind = topo->bitcoind; + wh->bitcoind = topo->ld->bitcoind; wh->minheight = blockscan_start; /* We're not happy, but we'll wait... */ log_broken(topo->ld->log, "bitcoind has gone backwards from %u to %u blocks, waiting...", blockscan_start, chaininfo->blockcount); - bitcoind_getchaininfo(wh, topo->bitcoind, blockscan_start, + bitcoind_getchaininfo(wh, topo->ld->bitcoind, blockscan_start, wait_until_height_reached, wh); ret = io_loop_with_timers(topo->ld); assert(ret == wh); /* Might have been a while, so re-ask for fee estimates */ - bitcoind_estimate_fees(feerates, topo->bitcoind, get_feerates_once, feerates); + bitcoind_estimate_fees(feerates, topo->ld->bitcoind, get_feerates_once, feerates); ret = io_loop_with_timers(topo->ld); assert(ret == topo); } } /* Sets bitcoin->synced or logs warnings */ - check_sync(topo->bitcoind, chaininfo->headercount, chaininfo->blockcount, + check_sync(topo->ld->bitcoind, chaininfo->headercount, chaininfo->blockcount, chaininfo->ibd, topo, true); /* It's very useful to have feerates early */ update_feerates(topo->ld, feerates->feerate_floor, feerates->rates, NULL); /* Get the first block, so we can initialize topography. */ - bitcoind_getrawblockbyheight(topo, topo->bitcoind, blockscan_start, + bitcoind_getrawblockbyheight(topo, topo->ld->bitcoind, blockscan_start, get_block_once, &blk); ret = io_loop_with_timers(topo->ld); assert(ret == topo); @@ -1378,7 +1377,7 @@ static void fixup_scan_block(struct bitcoind *bitcoind, } db_set_intvar(topo->ld->wallet->db, "fixup_block_scan", ++topo->old_block_scan); - bitcoind_getrawblockbyheight(topo, topo->bitcoind, + bitcoind_getrawblockbyheight(topo, topo->ld->bitcoind, topo->old_block_scan, fixup_scan_block, topo); } @@ -1386,7 +1385,7 @@ static void fixup_scan_block(struct bitcoind *bitcoind, static void fixup_scan(struct chain_topology *topo) { log_info(topo->ld->log, "Scanning for missed UTXOs from block %u", topo->old_block_scan); - bitcoind_getrawblockbyheight(topo, topo->bitcoind, + bitcoind_getrawblockbyheight(topo, topo->ld->bitcoind, topo->old_block_scan, fixup_scan_block, topo); } @@ -1394,7 +1393,7 @@ static void fixup_scan(struct chain_topology *topo) void begin_topology(struct chain_topology *topo) { /* If we were not synced, start looping to check */ - if (!topo->bitcoind->synced) + if (!topo->ld->bitcoind->synced) retry_sync(topo); /* Regular feerate updates */ start_fee_estimate(topo); diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index 9371d0f6849a..825e0548160d 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -91,9 +91,6 @@ struct chain_topology { * caught up. */ struct list_head *sync_waiters; - /* The bitcoind. */ - struct bitcoind *bitcoind; - /* Timers we're running. */ struct oneshot *checkchain_timer, *extend_timer, *updatefee_timer; diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index d3934e211a2b..3a00fc17db82 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -602,7 +602,7 @@ static void send_splice_tx_done(struct bitcoind *bitcoind UNUSED, if (!success) { info->err_msg = tal_strdup(info, msg); - bitcoind_getutxout(info, ld->topology->bitcoind, &outpoint, + bitcoind_getutxout(info, ld->bitcoind, &outpoint, check_utxo_block, info); } else { handle_tx_broadcast(info); @@ -634,8 +634,8 @@ static void send_splice_tx(struct channel *channel, info->err_msg = NULL; info->psbt = psbt; - bitcoind_sendrawtx(ld->topology->bitcoind, - ld->topology->bitcoind, + bitcoind_sendrawtx(ld->bitcoind, + ld->bitcoind, cc ? cc->cmd->id : NULL, tal_hex(tmpctx, tx_bytes), false, @@ -2196,7 +2196,7 @@ struct command_result *cancel_channel_before_broadcast(struct command *cmd, * the funding transaction isn't broadcast. We can't know if the funding * is broadcast by external wallet and the transaction hasn't * been onchain. */ - bitcoind_getutxout(cc, cmd->ld->topology->bitcoind, + bitcoind_getutxout(cc, cmd->ld->bitcoind, &cancel_channel->funding, process_check_funding_broadcast, /* Freed by callback */ diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index a3811abdd71b..aebdc6d34b85 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -1700,7 +1700,7 @@ static void sendfunding_done(struct bitcoind *bitcoind UNUSED, * that the broadcast would fail. Verify that's not * the case here. */ cs->err_msg = tal_strdup(cs, msg); - bitcoind_getutxout(cs, ld->topology->bitcoind, + bitcoind_getutxout(cs, ld->bitcoind, &channel->funding, check_utxo_block, cs); @@ -1736,8 +1736,8 @@ static void send_funding_tx(struct channel *channel, fmt_channel_id(tmpctx, &channel->cid), fmt_wally_tx(tmpctx, cs->wtx)); - bitcoind_sendrawtx(ld->topology->bitcoind, - ld->topology->bitcoind, + bitcoind_sendrawtx(ld->bitcoind, + ld->bitcoind, channel->open_attempt ? (channel->open_attempt->cmd ? channel->open_attempt->cmd->id @@ -2827,7 +2827,7 @@ static void validate_input_unspent(struct bitcoind *bitcoind, pv->next_index = i + 1; /* Confirm input is in a block */ - bitcoind_getutxout(pv, pv->channel->owner->ld->topology->bitcoind, + bitcoind_getutxout(pv, pv->channel->owner->ld->bitcoind, &outpoint, validate_input_unspent, pv); diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index fb8619c29bc5..2c0dcf141d0c 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -60,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -274,6 +275,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx) /*~ This is detailed in chaintopology.c */ ld->topology = new_topology(ld, ld->log); + ld->bitcoind = new_bitcoind(ld, ld, ld->log); ld->outgoing_txs = new_htable(ld, outgoing_tx_map); ld->rebroadcast_timer = NULL; ld->gossip_blockheight = 0; diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h index ab5fe729695f..5175925ebe8d 100644 --- a/lightningd/lightningd.h +++ b/lightningd/lightningd.h @@ -12,6 +12,7 @@ #include struct amount_msat; +struct bitcoind; struct oneshot; struct outgoing_tx_map; struct watchman; @@ -227,6 +228,9 @@ struct lightningd { /* Our chain topology. */ struct chain_topology *topology; + /* The bitcoind backend. */ + struct bitcoind *bitcoind; + /* Bitcoin transactions we're broadcasting */ struct outgoing_tx_map *outgoing_txs; struct oneshot *rebroadcast_timer; diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index e5d5f5806e2b..d193b6904ac0 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -559,7 +559,7 @@ void resend_opening_transactions(struct lightningd *ld) if (!wtx) continue; bitcoind_sendrawtx(channel, - ld->topology->bitcoind, + ld->bitcoind, NULL, tal_hex(tmpctx, linearize_wtx(tmpctx, wtx)), @@ -3441,7 +3441,7 @@ static struct command_result *json_getinfo(struct command *cmd, wallet_total_forward_fees(cmd->ld->wallet)); json_add_string(response, "lightning-dir", cmd->ld->config_netdir); - if (!cmd->ld->topology->bitcoind->synced) + if (!cmd->ld->bitcoind->synced) json_add_string(response, "warning_bitcoind_sync", "Bitcoind is not up-to-date with network."); else if (!topology_synced(cmd->ld->topology)) @@ -4085,7 +4085,7 @@ static struct command_result *json_dev_forget_channel(struct command *cmd, return command_check_done(cmd); if (!channel_state_uncommitted(forget->channel->state)) - bitcoind_getutxout(cmd, cmd->ld->topology->bitcoind, + bitcoind_getutxout(cmd, cmd->ld->bitcoind, &forget->channel->funding, process_dev_forget_channel, forget); return command_still_pending(cmd); diff --git a/lightningd/test/run-find_my_abspath.c b/lightningd/test/run-find_my_abspath.c index 1e014855cda4..fa934de25004 100644 --- a/lightningd/test/run-find_my_abspath.c +++ b/lightningd/test/run-find_my_abspath.c @@ -142,6 +142,11 @@ bool log_status_msg(struct logger *log UNNEEDED, const struct node_id *node_id UNNEEDED, const u8 *msg UNNEEDED) { fprintf(stderr, "log_status_msg called!\n"); abort(); } +/* Generated stub for new_bitcoind */ +struct bitcoind *new_bitcoind(const tal_t *ctx UNNEEDED, + struct lightningd *ld UNNEEDED, + struct logger *log UNNEEDED) +{ fprintf(stderr, "new_bitcoind called!\n"); abort(); } /* Generated stub for new_log_book */ struct log_book *new_log_book(struct lightningd *ld UNNEEDED) { fprintf(stderr, "new_log_book called!\n"); abort(); } diff --git a/lightningd/watch.c b/lightningd/watch.c index e18ea8d107fd..f22cf6983328 100644 --- a/lightningd/watch.c +++ b/lightningd/watch.c @@ -222,7 +222,7 @@ static bool txw_fire(struct txwatch *txw, depth ? "" : " REORG"); } txw->depth = depth; - r = txw->cb(txw->topo->bitcoind->ld, txid, txw->tx, txw->depth, + r = txw->cb(txw->topo->ld, txid, txw->tx, txw->depth, txw->cbarg); switch (r) { case DELETE_WATCH: diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 5f55e0f95ff3..84b21d427676 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -853,17 +853,17 @@ static struct command_result *json_chaininfo(struct command *cmd, log_unusual(cmd->ld->log, "Waiting for initial block download" " (this can take a while!)"); - cmd->ld->topology->bitcoind->synced = false; + cmd->ld->bitcoind->synced = false; } else if (*headercount != *blockcount) { log_unusual(cmd->ld->log, "Waiting for bitcoind to catch up" " (%u blocks of %u)", *blockcount, *headercount); - cmd->ld->topology->bitcoind->synced = false; + cmd->ld->bitcoind->synced = false; } else { - if (!cmd->ld->topology->bitcoind->synced) + if (!cmd->ld->bitcoind->synced) log_info(cmd->ld->log, "Bitcoin backend now synced"); - cmd->ld->topology->bitcoind->synced = true; + cmd->ld->bitcoind->synced = true; notify_new_block(cmd->ld); } @@ -871,7 +871,7 @@ static struct command_result *json_chaininfo(struct command *cmd, struct json_stream *response = json_stream_success(cmd); json_add_string(response, "chain", chain); - json_add_bool(response, "synced", cmd->ld->topology->bitcoind->synced); + json_add_bool(response, "synced", cmd->ld->bitcoind->synced); return command_success(cmd, response); } diff --git a/wallet/wallet.c b/wallet/wallet.c index 8b52cf22d9da..8e6bfd1b5ee0 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -7732,7 +7732,7 @@ void wallet_begin_old_close_rescan(struct lightningd *ld) /* This is not a leak, though it may take a while! */ tal_steal(ld, notleak(missing)); - bitcoind_getrawblockbyheight(missing, ld->topology->bitcoind, earliest_block, + bitcoind_getrawblockbyheight(missing, ld->bitcoind, earliest_block, mutual_close_p2pkh_catch, missing); } diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c index 700367cc6dd8..b170fa7dde63 100644 --- a/wallet/walletrpc.c +++ b/wallet/walletrpc.c @@ -557,7 +557,7 @@ static struct command_result *json_dev_rescan_outputs(struct command *cmd, json_array_end(rescan->response); return command_success(cmd, rescan->response); } - bitcoind_getutxout(rescan, cmd->ld->topology->bitcoind, + bitcoind_getutxout(rescan, cmd->ld->bitcoind, &rescan->utxos[0]->outpoint, process_utxo_result, rescan); @@ -1228,7 +1228,7 @@ static struct command_result *json_sendpsbt(struct command *cmd, } /* Now broadcast the transaction */ - bitcoind_sendrawtx(sending, cmd->ld->topology->bitcoind, + bitcoind_sendrawtx(sending, cmd->ld->bitcoind, cmd->id, tal_hex(tmpctx, linearize_wtx(tmpctx, sending->wtx)), From 049d08e96b3f3e066b17487f754dfe1b3e25093e Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 14:57:20 +0930 Subject: [PATCH 72/77] lightningd: extract feerate logic from chaintopology Part of the chaintopology spring clean. --- lightningd/anchorspend.c | 6 +- lightningd/chaintopology.c | 542 +----------------------------- lightningd/chaintopology.h | 40 +-- lightningd/channel_control.c | 22 +- lightningd/closing_control.c | 8 +- lightningd/dual_open_control.c | 12 +- lightningd/feerate.c | 581 ++++++++++++++++++++++++++++++++- lightningd/feerate.h | 49 +++ lightningd/onchain_control.c | 8 +- lightningd/opening_control.c | 10 +- lightningd/test/run-jsonrpc.c | 33 +- wallet/reservation.c | 8 +- 12 files changed, 674 insertions(+), 645 deletions(-) diff --git a/lightningd/anchorspend.c b/lightningd/anchorspend.c index 66a68b37e47b..757eb7cec5e9 100644 --- a/lightningd/anchorspend.c +++ b/lightningd/anchorspend.c @@ -240,7 +240,7 @@ static struct wally_psbt *anchor_psbt(const tal_t *ctx, /* PSBT knows how to spend utxos. */ psbt = psbt_using_utxos(ctx, ld->wallet, utxos, - default_locktime(ld->topology), + default_locktime(ld), BITCOIN_TX_RBF_SEQUENCE, NULL); /* BOLT #3: @@ -379,7 +379,7 @@ static struct bitcoin_tx *spend_anchor(const tal_t *ctx, if (!amount_msat_accumulate(&total_value, val->msat)) abort(); - feerate_target = feerate_for_target(ld->topology, val->block); + feerate_target = feerate_for_target(ld, val->block); /* If the feerate for the commitment tx is already * sufficient, don't try for anchor. */ @@ -451,7 +451,7 @@ static struct bitcoin_tx *spend_anchor(const tal_t *ctx, block_target = unimportant_deadline->block; if (block_target < get_block_height(ld->topology) + 12) block_target = get_block_height(ld->topology) + 12; - feerate_target = feerate_for_target(ld->topology, block_target); + feerate_target = feerate_for_target(ld, block_target); /* If the feerate for the commitment tx is already * sufficient, don't try for anchor. */ diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index e00cc83940ac..5816c58b17dd 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -196,456 +195,6 @@ static void watch_for_unconfirmed_txs(struct lightningd *ld, watch_unconfirmed_txid(ld, topo, &txids[i]); } -/* Mutual recursion via timer. */ -static void next_updatefee_timer(struct chain_topology *topo); - -bool unknown_feerates(const struct chain_topology *topo) -{ - return tal_count(topo->feerates[0]) == 0; -} - -static u32 interp_feerate(const struct feerate_est *rates, u32 blockcount) -{ - const struct feerate_est *before = NULL, *after = NULL; - - /* Find before and after. */ - const size_t num_feerates = tal_count(rates); - for (size_t i = 0; i < num_feerates; i++) { - if (rates[i].blockcount <= blockcount) { - before = &rates[i]; - } else if (rates[i].blockcount > blockcount && !after) { - after = &rates[i]; - } - } - /* No estimates at all? */ - if (!before && !after) - return 0; - /* We don't extrapolate. */ - if (!before && after) - return after->rate; - if (before && !after) - return before->rate; - - /* Interpolate, eg. blockcount 10, rate 15000, blockcount 20, rate 5000. - * At 15, rate should be 10000. - * 15000 + (15 - 10) / (20 - 10) * (15000 - 5000) - * 15000 + 5 / 10 * 10000 - * => 10000 - */ - /* Don't go backwards though! */ - if (before->rate < after->rate) - return before->rate; - - return before->rate - - ((u64)(blockcount - before->blockcount) - * (before->rate - after->rate) - / (after->blockcount - before->blockcount)); - -} - -u32 feerate_for_deadline(const struct chain_topology *topo, u32 blockcount) -{ - u32 rate = interp_feerate(topo->feerates[0], blockcount); - - /* 0 is a special value, meaning "don't know" */ - if (rate && rate < topo->feerate_floor) - rate = topo->feerate_floor; - return rate; -} - -u32 smoothed_feerate_for_deadline(const struct chain_topology *topo, - u32 blockcount) -{ - /* Note: we cap it at feerate_floor when we smooth */ - return interp_feerate(topo->smoothed_feerates, blockcount); -} - -/* feerate_for_deadline, but really lowball for distant targets */ -u32 feerate_for_target(const struct chain_topology *topo, u64 deadline) -{ - u64 blocks, blockheight; - - blockheight = get_block_height(topo); - - /* Past deadline? Want it now. */ - if (blockheight > deadline) - return feerate_for_deadline(topo, 1); - - blocks = deadline - blockheight; - - /* Over 200 blocks, we *always* use min fee! */ - if (blocks > 200) - return FEERATE_FLOOR; - /* Over 100 blocks, use min fee bitcoind will accept */ - if (blocks > 100) - return get_feerate_floor(topo); - - return feerate_for_deadline(topo, blocks); -} - -/* Mixes in fresh feerate rate into old smoothed values, modifies rate */ -static void smooth_one_feerate(const struct chain_topology *topo, - struct feerate_est *rate) -{ - /* Smoothing factor alpha for simple exponential smoothing. The goal is to - * have the feerate account for 90 percent of the values polled in the last - * 2 minutes. The following will do that in a polling interval - * independent manner. */ - double alpha = 1 - pow(0.1,(double)topo->poll_seconds / 120); - u32 old_feerate, feerate_smooth; - - /* We don't call this unless we had a previous feerate */ - old_feerate = smoothed_feerate_for_deadline(topo, rate->blockcount); - assert(old_feerate); - - feerate_smooth = rate->rate * alpha + old_feerate * (1 - alpha); - - /* But to avoid updating forever, only apply smoothing when its - * effect is more then 10 percent */ - if (abs((int)rate->rate - (int)feerate_smooth) > (0.1 * rate->rate)) - rate->rate = feerate_smooth; - - if (rate->rate < get_feerate_floor(topo)) - rate->rate = get_feerate_floor(topo); - - if (rate->rate != feerate_smooth) - log_debug(topo->log, - "Feerate estimate for %u blocks set to %u (was %u)", - rate->blockcount, rate->rate, feerate_smooth); -} - -static bool feerates_differ(const struct feerate_est *a, - const struct feerate_est *b) -{ - const size_t num_feerates = tal_count(a); - if (num_feerates != tal_count(b)) - return true; - for (size_t i = 0; i < num_feerates; i++) { - if (a[i].blockcount != b[i].blockcount) - return true; - if (a[i].rate != b[i].rate) - return true; - } - return false; -} - -/* In case the plugin does weird stuff! */ -static bool different_blockcounts(struct chain_topology *topo, - const struct feerate_est *old, - const struct feerate_est *new) -{ - const size_t num_feerates = tal_count(old); - if (num_feerates != tal_count(new)) { - log_unusual(topo->log, "Presented with %zu feerates this time (was %zu!)", - tal_count(new), num_feerates); - return true; - } - for (size_t i = 0; i < num_feerates; i++) { - if (old[i].blockcount != new[i].blockcount) { - log_unusual(topo->log, "Presented with feerates" - " for blockcount %u, previously %u", - new[i].blockcount, old[i].blockcount); - return true; - } - } - return false; -} - -static void update_feerates(struct lightningd *ld, - u32 feerate_floor, - const struct feerate_est *rates TAKES, - void *arg UNUSED) -{ - struct feerate_est *new_smoothed; - bool changed; - struct chain_topology *topo = ld->topology; - - topo->feerate_floor = feerate_floor; - - /* Don't bother updating if we got no feerates; we'd rather have - * historical ones, if any. */ - if (tal_count(rates) == 0) - return; - - /* If the feerate blockcounts differ, don't average, just override */ - if (topo->feerates[0] && different_blockcounts(topo, topo->feerates[0], rates)) { - for (size_t i = 0; i < ARRAY_SIZE(topo->feerates); i++) - topo->feerates[i] = tal_free(topo->feerates[i]); - topo->smoothed_feerates = tal_free(topo->smoothed_feerates); - } - - /* Move down historical rates, insert these */ - tal_free(topo->feerates[FEE_HISTORY_NUM-1]); - memmove(topo->feerates + 1, topo->feerates, - sizeof(topo->feerates[0]) * (FEE_HISTORY_NUM-1)); - topo->feerates[0] = tal_dup_talarr(topo, struct feerate_est, rates); - changed = feerates_differ(topo->feerates[0], topo->feerates[1]); - - /* Use this as basis of new smoothed ones. */ - new_smoothed = tal_dup_talarr(topo, struct feerate_est, topo->feerates[0]); - - /* If there were old smoothed feerates, incorporate those */ - if (tal_count(topo->smoothed_feerates) != 0) { - const size_t num_new = tal_count(new_smoothed); - for (size_t i = 0; i < num_new; i++) - smooth_one_feerate(topo, &new_smoothed[i]); - } - changed |= feerates_differ(topo->smoothed_feerates, new_smoothed); - tal_free(topo->smoothed_feerates); - topo->smoothed_feerates = new_smoothed; - - if (changed) - notify_feerate_change(topo->ld); -} - -static void update_feerates_repeat(struct lightningd *ld, - u32 feerate_floor, - const struct feerate_est *rates TAKES, - void *unused) -{ - update_feerates(ld, feerate_floor, rates, unused); - next_updatefee_timer(ld->topology); -} - -static void start_fee_estimate(struct chain_topology *topo) -{ - topo->updatefee_timer = NULL; - /* Based on timer, update fee estimates. */ - bitcoind_estimate_fees(topo->request_ctx, topo->ld->bitcoind, update_feerates_repeat, NULL); -} - -struct rate_conversion { - u32 blockcount; -}; - -static struct rate_conversion conversions[] = { - [FEERATE_OPENING] = { 12 }, - [FEERATE_MUTUAL_CLOSE] = { 100 }, - [FEERATE_UNILATERAL_CLOSE] = { 6 }, - [FEERATE_DELAYED_TO_US] = { 12 }, - [FEERATE_HTLC_RESOLUTION] = { 6 }, - [FEERATE_PENALTY] = { 12 }, -}; - -u32 opening_feerate(struct chain_topology *topo) -{ - if (topo->ld->force_feerates) - return topo->ld->force_feerates[FEERATE_OPENING]; - return feerate_for_deadline(topo, - conversions[FEERATE_OPENING].blockcount); -} - -u32 mutual_close_feerate(struct chain_topology *topo) -{ - if (topo->ld->force_feerates) - return topo->ld->force_feerates[FEERATE_MUTUAL_CLOSE]; - return smoothed_feerate_for_deadline(topo, - conversions[FEERATE_MUTUAL_CLOSE].blockcount); -} - -u32 unilateral_feerate(struct chain_topology *topo, bool option_anchors) -{ - if (topo->ld->force_feerates) - return topo->ld->force_feerates[FEERATE_UNILATERAL_CLOSE]; - - if (option_anchors) { - /* We can lowball fee, since we can CPFP with anchors */ - u32 feerate = feerate_for_deadline(topo, 100); - if (!feerate) - return 0; /* Don't know */ - /* We still need to get into the mempool, so use 5 sat/byte */ - if (feerate < 1250) - return 1250; - return feerate; - } - - return smoothed_feerate_for_deadline(topo, - conversions[FEERATE_UNILATERAL_CLOSE].blockcount) - * topo->ld->config.commit_fee_percent / 100; -} - -u32 delayed_to_us_feerate(struct chain_topology *topo) -{ - if (topo->ld->force_feerates) - return topo->ld->force_feerates[FEERATE_DELAYED_TO_US]; - return smoothed_feerate_for_deadline(topo, - conversions[FEERATE_DELAYED_TO_US].blockcount); -} - -u32 htlc_resolution_feerate(struct chain_topology *topo) -{ - if (topo->ld->force_feerates) - return topo->ld->force_feerates[FEERATE_HTLC_RESOLUTION]; - return smoothed_feerate_for_deadline(topo, - conversions[FEERATE_HTLC_RESOLUTION].blockcount); -} - -u32 penalty_feerate(struct chain_topology *topo) -{ - if (topo->ld->force_feerates) - return topo->ld->force_feerates[FEERATE_PENALTY]; - return smoothed_feerate_for_deadline(topo, - conversions[FEERATE_PENALTY].blockcount); -} - -u32 get_feerate_floor(const struct chain_topology *topo) -{ - return topo->feerate_floor; -} - -static struct command_result *json_feerates(struct command *cmd, - const char *buffer, - const jsmntok_t *obj UNNEEDED, - const jsmntok_t *params) -{ - struct chain_topology *topo = cmd->ld->topology; - struct json_stream *response; - enum feerate_style *style; - u32 rate; - - if (!param(cmd, buffer, params, - p_req("style", param_feerate_style, &style), - NULL)) - return command_param_failed(); - - const size_t num_feerates = tal_count(topo->feerates[0]); - - response = json_stream_success(cmd); - if (!num_feerates) - json_add_string(response, "warning_missing_feerates", - "Some fee estimates unavailable: bitcoind startup?"); - - json_object_start(response, feerate_style_name(*style)); - rate = opening_feerate(topo); - if (rate) - json_add_num(response, "opening", feerate_to_style(rate, *style)); - rate = mutual_close_feerate(topo); - if (rate) - json_add_num(response, "mutual_close", - feerate_to_style(rate, *style)); - rate = unilateral_feerate(topo, false); - if (rate) - json_add_num(response, "unilateral_close", - feerate_to_style(rate, *style)); - rate = unilateral_feerate(topo, true); - if (rate) - json_add_num(response, "unilateral_anchor_close", - feerate_to_style(rate, *style)); - rate = penalty_feerate(topo); - if (rate) - json_add_num(response, "penalty", - feerate_to_style(rate, *style)); - rate = unilateral_feerate(topo, true); - if (rate) { - rate += cmd->ld->config.feerate_offset; - if (rate > feerate_max(cmd->ld, NULL)) - rate = feerate_max(cmd->ld, NULL); - json_add_num(response, "splice", - feerate_to_style(rate, *style)); - } - - json_add_u64(response, "min_acceptable", - feerate_to_style(feerate_min(cmd->ld, NULL), *style)); - json_add_u64(response, "max_acceptable", - feerate_to_style(feerate_max(cmd->ld, NULL), *style)); - json_add_u64(response, "floor", - feerate_to_style(get_feerate_floor(cmd->ld->topology), - *style)); - - json_array_start(response, "estimates"); - assert(tal_count(topo->smoothed_feerates) == num_feerates); - for (size_t i = 0; i < num_feerates; i++) { - json_object_start(response, NULL); - json_add_num(response, "blockcount", - topo->feerates[0][i].blockcount); - json_add_u64(response, "feerate", - feerate_to_style(topo->feerates[0][i].rate, *style)); - json_add_u64(response, "smoothed_feerate", - feerate_to_style(topo->smoothed_feerates[i].rate, - *style)); - json_object_end(response); - } - json_array_end(response); - json_object_end(response); - - if (num_feerates) { - /* It actually is negotiated per-channel... */ - bool anchor_outputs - = feature_offered(cmd->ld->our_features->bits[INIT_FEATURE], - OPT_ANCHOR_OUTPUTS_DEPRECATED) - || feature_offered(cmd->ld->our_features->bits[INIT_FEATURE], - OPT_ANCHORS_ZERO_FEE_HTLC_TX); - - json_object_start(response, "onchain_fee_estimates"); - /* eg 020000000001016f51de645a47baa49a636b8ec974c28bdff0ac9151c0f4eda2dbe3b41dbe711d000000001716001401fad90abcd66697e2592164722de4a95ebee165ffffffff0240420f00000000002200205b8cd3b914cf67cdd8fa6273c930353dd36476734fbd962102c2df53b90880cdb73f890000000000160014c2ccab171c2a5be9dab52ec41b825863024c54660248304502210088f65e054dbc2d8f679de3e40150069854863efa4a45103b2bb63d060322f94702200d3ae8923924a458cffb0b7360179790830027bb6b29715ba03e12fc22365de1012103d745445c9362665f22e0d96e9e766f273f3260dea39c8a76bfa05dd2684ddccf00000000 == weight 702 */ - json_add_num(response, "opening_channel_satoshis", - opening_feerate(cmd->ld->topology) * 702 / 1000); - /* eg. 02000000000101afcfac637d44d4e0df52031dba55b18d3f1bd79ad4b7ebbee964f124c5163dc30100000000ffffffff02400d03000000000016001427213e2217b4f56bd19b6c8393dc9f61be691233ca1f0c0000000000160014071c49cad2f420f3c805f9f6b98a57269cb1415004004830450221009a12b4d5ae1d41781f79bedecfa3e65542b1799a46c272287ba41f009d2e27ff0220382630c899207487eba28062f3989c4b656c697c23a8c89c1d115c98d82ff261014730440220191ddf13834aa08ea06dca8191422e85d217b065462d1b405b665eefa0684ed70220252409bf033eeab3aae89ae27596d7e0491bcc7ae759c5644bced71ef3cccef30147522102324266de8403b3ab157a09f1f784d587af61831c998c151bcc21bb74c2b2314b2102e3bd38009866c9da8ec4aa99cc4ea9c6c0dd46df15c61ef0ce1f271291714e5752ae00000000 == weight 673 */ - json_add_u64(response, "mutual_close_satoshis", - mutual_close_feerate(cmd->ld->topology) * 673 / 1000); - /* eg. 02000000000101c4fecaae1ea940c15ec502de732c4c386d51f981317605bbe5ad2c59165690ab00000000009db0e280010a2d0f00000000002200208d290003cedb0dd00cd5004c2d565d55fc70227bf5711186f4fa9392f8f32b4a0400483045022100952fcf8c730c91cf66bcb742cd52f046c0db3694dc461e7599be330a22466d790220740738a6f9d9e1ae5c86452fa07b0d8dddc90f8bee4ded24a88fe4b7400089eb01483045022100db3002a93390fc15c193da57d6ce1020e82705e760a3aa935ebe864bd66dd8e8022062ee9c6aa7b88ff4580e2671900a339754116371d8f40eba15b798136a76cd150147522102324266de8403b3ab157a09f1f784d587af61831c998c151bcc21bb74c2b2314b2102e3bd38009866c9da8ec4aa99cc4ea9c6c0dd46df15c61ef0ce1f271291714e5752ae9a3ed620 == weight 598 */ - /* Or, with anchors: - * 02000000000101dc824e8e880f90f397a74f89022b4d58f8c36ebc4fffc238bd525bd11f5002a501000000009db0e280044a010000000000002200200e1a08b3da3bea6a7a77315f95afcd589fe799af46cf9bfb89523172814050e44a01000000000000220020be7935a77ca9ab70a4b8b1906825637767fed3c00824aa90c988983587d6848878e001000000000022002009fa3082e61ca0bd627915b53b0cb8afa467248fa4dc95141f78b96e9c98a8ed245a0d000000000022002091fb9e7843a03e66b4b1173482a0eb394f03a35aae4c28e8b4b1f575696bd793040047304402205c2ea9cf6f670e2f454c054f9aaca2d248763e258e44c71675c06135fd8f36cb02201b564f0e1b3f1ea19342f26e978a4981675da23042b4d392737636738c3514da0147304402205fcd2af5b724cbbf71dfa07bd14e8018ce22c08a019976dc03d0f545f848d0a702203652200350cadb464a70a09829d09227ed3da8c6b8ef5e3a59b5eefd056deaae0147522102324266de8403b3ab157a09f1f784d587af61831c998c151bcc21bb74c2b2314b2102e3bd38009866c9da8ec4aa99cc4ea9c6c0dd46df15c61ef0ce1f271291714e5752ae9b3ed620 1112 */ - if (anchor_outputs) - json_add_u64(response, "unilateral_close_satoshis", - unilateral_feerate(cmd->ld->topology, true) * 1112 / 1000); - else - json_add_u64(response, "unilateral_close_satoshis", - unilateral_feerate(cmd->ld->topology, false) * 598 / 1000); - json_add_u64(response, "unilateral_close_nonanchor_satoshis", - unilateral_feerate(cmd->ld->topology, false) * 598 / 1000); - - json_add_u64(response, "htlc_timeout_satoshis", - htlc_timeout_fee(htlc_resolution_feerate(cmd->ld->topology), - false, false).satoshis /* Raw: estimate */); - json_add_u64(response, "htlc_success_satoshis", - htlc_success_fee(htlc_resolution_feerate(cmd->ld->topology), - false, false).satoshis /* Raw: estimate */); - json_object_end(response); - } - - return command_success(cmd, response); -} - -static const struct json_command feerates_command = { - "feerates", - json_feerates, -}; -AUTODATA(json_command, &feerates_command); - -static struct command_result *json_parse_feerate(struct command *cmd, - const char *buffer, - const jsmntok_t *obj UNNEEDED, - const jsmntok_t *params) -{ - struct json_stream *response; - u32 *feerate; - - if (!param(cmd, buffer, params, - p_req("feerate", param_feerate, &feerate), - NULL)) - return command_param_failed(); - - response = json_stream_success(cmd); - json_add_num(response, feerate_style_name(FEERATE_PER_KSIPA), - feerate_to_style(*feerate, FEERATE_PER_KSIPA)); - return command_success(cmd, response); -} - -static const struct json_command parse_feerate_command = { - "parsefeerate", - json_parse_feerate, -}; -AUTODATA(json_command, &parse_feerate_command); - -static void next_updatefee_timer(struct chain_topology *topo) -{ - assert(!topo->updatefee_timer); - topo->updatefee_timer = new_reltimer(topo->ld->timers, topo, - time_from_sec(topo->poll_seconds), - start_fee_estimate, topo); -} - struct sync_waiter { /* Linked from chain_topology->sync_waiters */ struct list_node list; @@ -953,95 +502,6 @@ u32 get_network_blockheight(const struct chain_topology *topo) return topo->headercount; } -u32 feerate_min(struct lightningd *ld, bool *unknown) -{ - const struct chain_topology *topo = ld->topology; - u32 min; - - if (unknown) - *unknown = false; - - /* We allow the user to ignore the fee limits, - * although this comes with inherent risks. - * - * By enabling this option, users are explicitly - * made aware of the potential dangers. - * There are situations, such as the one described in [1], - * where it becomes necessary to bypass the fee limits to resolve - * issues like a stuck channel. - * - * BTW experimental-anchors feature provides a solution to this problem. - * - * [1] https://github.com/ElementsProject/lightning/issues/6362 - * */ - min = 0xFFFFFFFF; - for (size_t i = 0; i < ARRAY_SIZE(topo->feerates); i++) { - const size_t num_feerates = tal_count(topo->feerates[i]); - for (size_t j = 0; j < num_feerates; j++) { - if (topo->feerates[i][j].rate < min) - min = topo->feerates[i][j].rate; - } - } - if (min == 0xFFFFFFFF) { - if (unknown) - *unknown = true; - min = 0; - } - - /* FIXME: This is what bcli used to do: halve the slow feerate! */ - min /= 2; - - /* We can't allow less than feerate_floor, since that won't relay */ - if (min < get_feerate_floor(topo)) - return get_feerate_floor(topo); - return min; -} - -u32 feerate_max(struct lightningd *ld, bool *unknown) -{ - const struct chain_topology *topo = ld->topology; - u32 max = 0; - - if (unknown) - *unknown = false; - - for (size_t i = 0; i < ARRAY_SIZE(topo->feerates); i++) { - const size_t num_feerates = tal_count(topo->feerates[i]); - for (size_t j = 0; j < num_feerates; j++) { - if (topo->feerates[i][j].rate > max) - max = topo->feerates[i][j].rate; - } - } - if (!max) { - if (unknown) - *unknown = true; - return UINT_MAX; - } - return max * topo->ld->config.max_fee_multiplier; -} - -u32 default_locktime(const struct chain_topology *topo) -{ - u32 locktime, current_height = get_block_height(topo); - - /* Setting the locktime to the next block to be mined has multiple - * benefits: - * - anti fee-snipping (even if not yet likely) - * - less distinguishable transactions (with this we create - * general-purpose transactions which looks like bitcoind: - * native segwit, nlocktime set to tip, and sequence set to - * 0xFFFFFFFD by default. Other wallets are likely to implement - * this too). - */ - locktime = current_height; - - /* Eventually fuzz it too. */ - if (locktime > 100 && pseudorand(10) == 0) - locktime -= pseudorand(100); - - return locktime; -} - /* On shutdown, channels get deleted last. That frees from our list, so * do it now instead. */ static void destroy_chain_topology(struct chain_topology *topo) @@ -1396,7 +856,7 @@ void begin_topology(struct chain_topology *topo) if (!topo->ld->bitcoind->synced) retry_sync(topo); /* Regular feerate updates */ - start_fee_estimate(topo); + start_fee_polling(topo->ld); /* Regular block updates */ try_extend_tip(topo); diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index 825e0548160d..99215464f87e 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -2,6 +2,7 @@ #define LIGHTNING_LIGHTNINGD_CHAINTOPOLOGY_H #include "config.h" #include +#include #include struct bitcoin_tx; @@ -57,12 +58,6 @@ static inline bool block_eq(const struct block *b, const struct bitcoin_blkid *k HTABLE_DEFINE_NODUPS_TYPE(struct block, keyof_block_map, hash_sha, block_eq, block_map); -/* Our plugins give us a series of blockcount, feerate pairs. */ -struct feerate_est { - u32 blockcount; - u32 rate; -}; - struct chain_topology { struct lightningd *ld; struct block *root; @@ -121,9 +116,6 @@ struct txlocator { u32 index; }; -/* Get the minimum feerate that bitcoind will accept */ -u32 get_feerate_floor(const struct chain_topology *topo); - /* This is the number of blocks which would have to be mined to invalidate * the tx */ size_t get_tx_depth(const struct chain_topology *topo, @@ -139,33 +131,6 @@ u32 get_block_height(const struct chain_topology *topo); * likely to lag behind the rest of the network.*/ u32 get_network_blockheight(const struct chain_topology *topo); -/* Get feerate estimate for getting a tx in this many blocks */ -u32 feerate_for_deadline(const struct chain_topology *topo, u32 blockcount); -u32 smoothed_feerate_for_deadline(const struct chain_topology *topo, u32 blockcount); - -/* Get feerate to hit this *block number*. */ -u32 feerate_for_target(const struct chain_topology *topo, u64 deadline); - -/* Has our feerate estimation failed altogether? */ -bool unknown_feerates(const struct chain_topology *topo); - -/* Get range of feerates to insist other side abide by for normal channels. - * If we have to guess, sets *unknown to true, otherwise false. */ -u32 feerate_min(struct lightningd *ld, bool *unknown); -u32 feerate_max(struct lightningd *ld, bool *unknown); - -/* These return 0 if unknown */ -u32 opening_feerate(struct chain_topology *topo); -u32 mutual_close_feerate(struct chain_topology *topo); -u32 unilateral_feerate(struct chain_topology *topo, bool option_anchors); -/* For onchain resolution. */ -u32 delayed_to_us_feerate(struct chain_topology *topo); -u32 htlc_resolution_feerate(struct chain_topology *topo); -u32 penalty_feerate(struct chain_topology *topo); - -/* Usually we set nLocktime to tip (or recent) like bitcoind does */ -u32 default_locktime(const struct chain_topology *topo); - struct chain_topology *new_topology(struct lightningd *ld, struct logger *log); void setup_topology(struct chain_topology *topology); @@ -203,9 +168,6 @@ void topology_add_sync_waiter_(const tal_t *ctx, (arg)) -/* In channel_control.c */ -void notify_feerate_change(struct lightningd *ld); - /* We want to update db when this txid is confirmed. We always do this * if it's related to a channel or incoming funds, but sendpsbt without * change would be otherwise untracked. */ diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index 3a00fc17db82..335950d9bce5 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -56,7 +56,7 @@ static u32 default_feerate(struct lightningd *ld, const struct channel *channel, { u32 max_feerate; bool anchors = channel_type_has_anchors(channel->type); - u32 feerate = unilateral_feerate(ld->topology, anchors); + u32 feerate = unilateral_feerate(ld, anchors); /* Nothing to do if we don't know feerate. */ if (!feerate) @@ -89,7 +89,7 @@ void channel_update_feerates(struct lightningd *ld, const struct channel *channe /* For anchors, we just need the commitment tx to relay. */ if (anchors) - min_feerate = get_feerate_floor(ld->topology); + min_feerate = get_feerate_floor(ld); else min_feerate = feerate_min(ld, NULL); max_feerate = feerate_max(ld, NULL); @@ -105,15 +105,15 @@ void channel_update_feerates(struct lightningd *ld, const struct channel *channe feerate, min_feerate, feerate_max(ld, NULL), - penalty_feerate(ld->topology), - opening_feerate(ld->topology), + penalty_feerate(ld), + opening_feerate(ld), feerate_splice); msg = towire_channeld_feerates(NULL, feerate, min_feerate, max_feerate, - penalty_feerate(ld->topology), - opening_feerate(ld->topology), + penalty_feerate(ld), + opening_feerate(ld), feerate_splice); subd_send_msg(channel->owner, take(msg)); } @@ -1766,7 +1766,7 @@ bool peer_start_channeld(struct channel *channel, /* For anchors, we just need the commitment tx to relay. */ if (channel_type_has_anchors(channel->type)) - min_feerate = get_feerate_floor(ld->topology); + min_feerate = get_feerate_floor(ld); else min_feerate = feerate_min(ld, NULL); max_feerate = feerate_max(ld, NULL); @@ -1847,8 +1847,8 @@ bool peer_start_channeld(struct channel *channel, feerate_splice, min_feerate, max_feerate, - penalty_feerate(ld->topology), - opening_feerate(ld->topology), + penalty_feerate(ld), + opening_feerate(ld), &channel->last_sig, &channel->channel_info.remote_fundingkey, &channel->channel_info.theirbase, @@ -2669,8 +2669,8 @@ static struct command_result *json_dev_feerate(struct command *cmd, msg = towire_channeld_feerates(NULL, *feerate, feerate_min(cmd->ld, NULL), feerate_max(cmd->ld, NULL), - penalty_feerate(cmd->ld->topology), - opening_feerate(cmd->ld->topology), + penalty_feerate(cmd->ld), + opening_feerate(cmd->ld), default_feerate(cmd->ld, channel, true)); subd_send_msg(channel->owner, take(msg)); diff --git a/lightningd/closing_control.c b/lightningd/closing_control.c index b80821ff80f9..a04c0cc428ef 100644 --- a/lightningd/closing_control.c +++ b/lightningd/closing_control.c @@ -412,15 +412,15 @@ void peer_start_closingd(struct channel *channel, struct peer_fd *peer_fd) channel->opener, LOCAL); /* If we can't determine feerate, start at half unilateral feerate. */ - feerate = mutual_close_feerate(ld->topology); + feerate = mutual_close_feerate(ld); if (!feerate) { feerate = final_commit_feerate / 2; - if (feerate < get_feerate_floor(ld->topology)) - feerate = get_feerate_floor(ld->topology); + if (feerate < get_feerate_floor(ld)) + feerate = get_feerate_floor(ld); } /* Aim for reasonable max, but use final if we don't know. */ - max_feerate = unilateral_feerate(ld->topology, false); + max_feerate = unilateral_feerate(ld, false); if (!max_feerate) max_feerate = final_commit_feerate; diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index aebdc6d34b85..a7d9634613f9 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -2062,7 +2062,7 @@ static void accepter_got_offer(struct subd *dualopend, /* Don't allow opening if we don't know any fees; even if * ignore-feerates is set. */ - if (unknown_feerates(dualopend->ld->topology)) { + if (unknown_feerates(dualopend->ld)) { subd_send_msg(dualopend, take(towire_dualopend_fail(NULL, "Cannot accept channel: feerates unknown"))); tal_free(payload); @@ -2997,7 +2997,7 @@ static struct command_result *init_set_feerate(struct command *cmd, { if (!*feerate_per_kw_funding) { *feerate_per_kw_funding = tal(cmd, u32); - **feerate_per_kw_funding = opening_feerate(cmd->ld->topology); + **feerate_per_kw_funding = opening_feerate(cmd->ld); if (!**feerate_per_kw_funding) return command_fail(cmd, LIGHTNINGD, "`funding_feerate` not specified and fee " @@ -3074,9 +3074,9 @@ static struct command_result *openchannel_init(struct command *cmd, our_upfront_shutdown_script_wallet_index = NULL; /* 0 from this means "unknown" */ - anchor_feerate = unilateral_feerate(cmd->ld->topology, true); + anchor_feerate = unilateral_feerate(cmd->ld, true); if (anchor_feerate == 0) { - anchor_feerate = get_feerate_floor(cmd->ld->topology); + anchor_feerate = get_feerate_floor(cmd->ld); assert(anchor_feerate); } @@ -3879,9 +3879,9 @@ static struct command_result *json_queryrates(struct command *cmd, our_upfront_shutdown_script_wallet_index = NULL; /* 0 from this means "unknown" */ - anchor_feerate = unilateral_feerate(cmd->ld->topology, true); + anchor_feerate = unilateral_feerate(cmd->ld, true); if (anchor_feerate == 0) { - anchor_feerate = get_feerate_floor(cmd->ld->topology); + anchor_feerate = get_feerate_floor(cmd->ld); assert(anchor_feerate); } diff --git a/lightningd/feerate.c b/lightningd/feerate.c index 78027a56a4c2..fb43d464bbf9 100644 --- a/lightningd/feerate.c +++ b/lightningd/feerate.c @@ -1,9 +1,23 @@ #include "config.h" +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include #include #include #include #include +#include +#include +#include const char *feerate_name(enum feerate feerate) { @@ -55,23 +69,23 @@ static struct command_result *param_feerate_unchecked(struct command *cmd, *feerate = tal(cmd, u32); if (json_tok_streq(buffer, tok, "opening")) { - **feerate = opening_feerate(cmd->ld->topology); + **feerate = opening_feerate(cmd->ld); return NULL; } if (json_tok_streq(buffer, tok, "mutual_close")) { - **feerate = mutual_close_feerate(cmd->ld->topology); + **feerate = mutual_close_feerate(cmd->ld); return NULL; } if (json_tok_streq(buffer, tok, "penalty")) { - **feerate = penalty_feerate(cmd->ld->topology); + **feerate = penalty_feerate(cmd->ld); return NULL; } if (json_tok_streq(buffer, tok, "unilateral_close")) { - **feerate = unilateral_feerate(cmd->ld->topology, false); + **feerate = unilateral_feerate(cmd->ld, false); return NULL; } if (json_tok_streq(buffer, tok, "unilateral_anchor_close")) { - **feerate = unilateral_feerate(cmd->ld->topology, true); + **feerate = unilateral_feerate(cmd->ld, true); return NULL; } @@ -79,16 +93,16 @@ static struct command_result *param_feerate_unchecked(struct command *cmd, * and many commands rely on this syntax now. * It's also really more natural for an user interface. */ if (json_tok_streq(buffer, tok, "slow")) { - **feerate = feerate_for_deadline(cmd->ld->topology, 100); + **feerate = feerate_for_deadline(cmd->ld, 100); return NULL; } else if (json_tok_streq(buffer, tok, "normal")) { - **feerate = feerate_for_deadline(cmd->ld->topology, 12); + **feerate = feerate_for_deadline(cmd->ld, 12); return NULL; } else if (json_tok_streq(buffer, tok, "urgent")) { - **feerate = feerate_for_deadline(cmd->ld->topology, 6); + **feerate = feerate_for_deadline(cmd->ld, 6); return NULL; } else if (json_tok_streq(buffer, tok, "minimum")) { - **feerate = get_feerate_floor(cmd->ld->topology); + **feerate = get_feerate_floor(cmd->ld); return NULL; } @@ -104,7 +118,7 @@ static struct command_result *param_feerate_unchecked(struct command *cmd, name, base.end - base.start, buffer + base.start); } - **feerate = feerate_for_deadline(cmd->ld->topology, numblocks); + **feerate = feerate_for_deadline(cmd->ld, numblocks); return NULL; } @@ -163,3 +177,550 @@ struct command_result *param_feerate_val(struct command *cmd, **feerate_per_kw = FEERATE_FLOOR; return NULL; } + +/* Mutual recursion via timer. */ +static void next_updatefee_timer(struct lightningd *ld); + +bool unknown_feerates(const struct lightningd *ld) +{ + return tal_count(ld->topology->feerates[0]) == 0; +} + +static u32 interp_feerate(const struct feerate_est *rates, u32 blockcount) +{ + const struct feerate_est *before = NULL, *after = NULL; + + /* Find before and after. */ + const size_t num_feerates = tal_count(rates); + for (size_t i = 0; i < num_feerates; i++) { + if (rates[i].blockcount <= blockcount) { + before = &rates[i]; + } else if (rates[i].blockcount > blockcount && !after) { + after = &rates[i]; + } + } + /* No estimates at all? */ + if (!before && !after) + return 0; + /* We don't extrapolate. */ + if (!before && after) + return after->rate; + if (before && !after) + return before->rate; + + /* Interpolate, eg. blockcount 10, rate 15000, blockcount 20, rate 5000. + * At 15, rate should be 10000. + * 15000 + (15 - 10) / (20 - 10) * (15000 - 5000) + * 15000 + 5 / 10 * 10000 + * => 10000 + */ + /* Don't go backwards though! */ + if (before->rate < after->rate) + return before->rate; + + return before->rate + - ((u64)(blockcount - before->blockcount) + * (before->rate - after->rate) + / (after->blockcount - before->blockcount)); + +} + +u32 feerate_for_deadline(const struct lightningd *ld, u32 blockcount) +{ + u32 rate = interp_feerate(ld->topology->feerates[0], blockcount); + + /* 0 is a special value, meaning "don't know" */ + if (rate && rate < ld->topology->feerate_floor) + rate = ld->topology->feerate_floor; + return rate; +} + +u32 smoothed_feerate_for_deadline(const struct lightningd *ld, + u32 blockcount) +{ + /* Note: we cap it at feerate_floor when we smooth */ + return interp_feerate(ld->topology->smoothed_feerates, blockcount); +} + +/* feerate_for_deadline, but really lowball for distant targets */ +u32 feerate_for_target(const struct lightningd *ld, u64 deadline) +{ + u64 blocks, blockheight; + + blockheight = get_block_height(ld->topology); + + /* Past deadline? Want it now. */ + if (blockheight > deadline) + return feerate_for_deadline(ld, 1); + + blocks = deadline - blockheight; + + /* Over 200 blocks, we *always* use min fee! */ + if (blocks > 200) + return FEERATE_FLOOR; + /* Over 100 blocks, use min fee bitcoind will accept */ + if (blocks > 100) + return get_feerate_floor(ld); + + return feerate_for_deadline(ld, blocks); +} + +/* Mixes in fresh feerate rate into old smoothed values, modifies rate */ +static void smooth_one_feerate(const struct lightningd *ld, + struct feerate_est *rate) +{ + /* Smoothing factor alpha for simple exponential smoothing. The goal is to + * have the feerate account for 90 percent of the values polled in the last + * 2 minutes. The following will do that in a polling interval + * independent manner. */ + double alpha = 1 - pow(0.1,(double)ld->topology->poll_seconds / 120); + u32 old_feerate, feerate_smooth; + + /* We don't call this unless we had a previous feerate */ + old_feerate = smoothed_feerate_for_deadline(ld, rate->blockcount); + assert(old_feerate); + + feerate_smooth = rate->rate * alpha + old_feerate * (1 - alpha); + + /* But to avoid updating forever, only apply smoothing when its + * effect is more then 10 percent */ + if (abs((int)rate->rate - (int)feerate_smooth) > (0.1 * rate->rate)) + rate->rate = feerate_smooth; + + if (rate->rate < get_feerate_floor(ld)) + rate->rate = get_feerate_floor(ld); + + if (rate->rate != feerate_smooth) + log_debug(ld->topology->log, + "Feerate estimate for %u blocks set to %u (was %u)", + rate->blockcount, rate->rate, feerate_smooth); +} + +static bool feerates_differ(const struct feerate_est *a, + const struct feerate_est *b) +{ + const size_t num_feerates = tal_count(a); + if (num_feerates != tal_count(b)) + return true; + for (size_t i = 0; i < num_feerates; i++) { + if (a[i].blockcount != b[i].blockcount) + return true; + if (a[i].rate != b[i].rate) + return true; + } + return false; +} + +/* In case the plugin does weird stuff! */ +static bool different_blockcounts(struct lightningd *ld, + const struct feerate_est *old, + const struct feerate_est *new) +{ + const size_t num_feerates = tal_count(old); + if (num_feerates != tal_count(new)) { + log_unusual(ld->topology->log, + "Presented with %zu feerates this time (was %zu!)", + tal_count(new), num_feerates); + return true; + } + for (size_t i = 0; i < num_feerates; i++) { + if (old[i].blockcount != new[i].blockcount) { + log_unusual(ld->topology->log, + "Presented with feerates" + " for blockcount %u, previously %u", + new[i].blockcount, old[i].blockcount); + return true; + } + } + return false; +} + +void update_feerates(struct lightningd *ld, + u32 feerate_floor, + const struct feerate_est *rates TAKES, + void *arg UNUSED) +{ + struct feerate_est *new_smoothed; + bool changed; + struct chain_topology *topo = ld->topology; + + topo->feerate_floor = feerate_floor; + + /* Don't bother updating if we got no feerates; we'd rather have + * historical ones, if any. */ + if (tal_count(rates) == 0) + return; + + /* If the feerate blockcounts differ, don't average, just override */ + if (topo->feerates[0] && different_blockcounts(ld, topo->feerates[0], rates)) { + for (size_t i = 0; i < ARRAY_SIZE(topo->feerates); i++) + topo->feerates[i] = tal_free(topo->feerates[i]); + topo->smoothed_feerates = tal_free(topo->smoothed_feerates); + } + + /* Move down historical rates, insert these */ + tal_free(topo->feerates[FEE_HISTORY_NUM-1]); + memmove(topo->feerates + 1, topo->feerates, + sizeof(topo->feerates[0]) * (FEE_HISTORY_NUM-1)); + topo->feerates[0] = tal_dup_talarr(topo, struct feerate_est, rates); + changed = feerates_differ(topo->feerates[0], topo->feerates[1]); + + /* Use this as basis of new smoothed ones. */ + new_smoothed = tal_dup_talarr(topo, struct feerate_est, topo->feerates[0]); + + /* If there were old smoothed feerates, incorporate those */ + if (tal_count(topo->smoothed_feerates) != 0) { + const size_t num_new = tal_count(new_smoothed); + for (size_t i = 0; i < num_new; i++) + smooth_one_feerate(ld, &new_smoothed[i]); + } + changed |= feerates_differ(topo->smoothed_feerates, new_smoothed); + tal_free(topo->smoothed_feerates); + topo->smoothed_feerates = new_smoothed; + + if (changed) + notify_feerate_change(ld); +} + +static void update_feerates_repeat(struct lightningd *ld, + u32 feerate_floor, + const struct feerate_est *rates TAKES, + void *unused) +{ + update_feerates(ld, feerate_floor, rates, unused); + next_updatefee_timer(ld); +} + +static void start_fee_estimate(struct lightningd *ld) +{ + ld->topology->updatefee_timer = NULL; + /* Based on timer, update fee estimates. */ + bitcoind_estimate_fees(ld->topology->request_ctx, ld->bitcoind, + update_feerates_repeat, NULL); +} + +void start_fee_polling(struct lightningd *ld) +{ + start_fee_estimate(ld); +} + +static void next_updatefee_timer(struct lightningd *ld) +{ + assert(!ld->topology->updatefee_timer); + ld->topology->updatefee_timer + = new_reltimer(ld->timers, ld, + time_from_sec(ld->topology->poll_seconds), + start_fee_estimate, ld); +} + +struct rate_conversion { + u32 blockcount; +}; + +static struct rate_conversion conversions[] = { + [FEERATE_OPENING] = { 12 }, + [FEERATE_MUTUAL_CLOSE] = { 100 }, + [FEERATE_UNILATERAL_CLOSE] = { 6 }, + [FEERATE_DELAYED_TO_US] = { 12 }, + [FEERATE_HTLC_RESOLUTION] = { 6 }, + [FEERATE_PENALTY] = { 12 }, +}; + +u32 opening_feerate(struct lightningd *ld) +{ + if (ld->force_feerates) + return ld->force_feerates[FEERATE_OPENING]; + return feerate_for_deadline(ld, + conversions[FEERATE_OPENING].blockcount); +} + +u32 mutual_close_feerate(struct lightningd *ld) +{ + if (ld->force_feerates) + return ld->force_feerates[FEERATE_MUTUAL_CLOSE]; + return smoothed_feerate_for_deadline(ld, + conversions[FEERATE_MUTUAL_CLOSE].blockcount); +} + +u32 unilateral_feerate(struct lightningd *ld, bool option_anchors) +{ + if (ld->force_feerates) + return ld->force_feerates[FEERATE_UNILATERAL_CLOSE]; + + if (option_anchors) { + /* We can lowball fee, since we can CPFP with anchors */ + u32 feerate = feerate_for_deadline(ld, 100); + if (!feerate) + return 0; /* Don't know */ + /* We still need to get into the mempool, so use 5 sat/byte */ + if (feerate < 1250) + return 1250; + return feerate; + } + + return smoothed_feerate_for_deadline(ld, + conversions[FEERATE_UNILATERAL_CLOSE].blockcount) + * ld->config.commit_fee_percent / 100; +} + +u32 delayed_to_us_feerate(struct lightningd *ld) +{ + if (ld->force_feerates) + return ld->force_feerates[FEERATE_DELAYED_TO_US]; + return smoothed_feerate_for_deadline(ld, + conversions[FEERATE_DELAYED_TO_US].blockcount); +} + +u32 htlc_resolution_feerate(struct lightningd *ld) +{ + if (ld->force_feerates) + return ld->force_feerates[FEERATE_HTLC_RESOLUTION]; + return smoothed_feerate_for_deadline(ld, + conversions[FEERATE_HTLC_RESOLUTION].blockcount); +} + +u32 penalty_feerate(struct lightningd *ld) +{ + if (ld->force_feerates) + return ld->force_feerates[FEERATE_PENALTY]; + return smoothed_feerate_for_deadline(ld, + conversions[FEERATE_PENALTY].blockcount); +} + +u32 get_feerate_floor(const struct lightningd *ld) +{ + return ld->topology->feerate_floor; +} + +u32 feerate_min(struct lightningd *ld, bool *unknown) +{ + const struct chain_topology *topo = ld->topology; + u32 min; + + if (unknown) + *unknown = false; + + /* We allow the user to ignore the fee limits, + * although this comes with inherent risks. + * + * By enabling this option, users are explicitly + * made aware of the potential dangers. + * There are situations, such as the one described in [1], + * where it becomes necessary to bypass the fee limits to resolve + * issues like a stuck channel. + * + * BTW experimental-anchors feature provides a solution to this problem. + * + * [1] https://github.com/ElementsProject/lightning/issues/6362 + * */ + min = 0xFFFFFFFF; + for (size_t i = 0; i < ARRAY_SIZE(topo->feerates); i++) { + const size_t num_feerates = tal_count(topo->feerates[i]); + for (size_t j = 0; j < num_feerates; j++) { + if (topo->feerates[i][j].rate < min) + min = topo->feerates[i][j].rate; + } + } + if (min == 0xFFFFFFFF) { + if (unknown) + *unknown = true; + min = 0; + } + + /* FIXME: This is what bcli used to do: halve the slow feerate! */ + min /= 2; + + /* We can't allow less than feerate_floor, since that won't relay */ + if (min < get_feerate_floor(ld)) + return get_feerate_floor(ld); + return min; +} + +u32 feerate_max(struct lightningd *ld, bool *unknown) +{ + const struct chain_topology *topo = ld->topology; + u32 max = 0; + + if (unknown) + *unknown = false; + + for (size_t i = 0; i < ARRAY_SIZE(topo->feerates); i++) { + const size_t num_feerates = tal_count(topo->feerates[i]); + for (size_t j = 0; j < num_feerates; j++) { + if (topo->feerates[i][j].rate > max) + max = topo->feerates[i][j].rate; + } + } + if (!max) { + if (unknown) + *unknown = true; + return UINT_MAX; + } + return max * ld->config.max_fee_multiplier; +} + +u32 default_locktime(const struct lightningd *ld) +{ + u32 locktime, current_height = get_block_height(ld->topology); + + /* Setting the locktime to the next block to be mined has multiple + * benefits: + * - anti fee-snipping (even if not yet likely) + * - less distinguishable transactions (with this we create + * general-purpose transactions which looks like bitcoind: + * native segwit, nlocktime set to tip, and sequence set to + * 0xFFFFFFFD by default. Other wallets are likely to implement + * this too). + */ + locktime = current_height; + + /* Eventually fuzz it too. */ + if (locktime > 100 && pseudorand(10) == 0) + locktime -= pseudorand(100); + + return locktime; +} + +static struct command_result *json_feerates(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct lightningd *ld = cmd->ld; + struct json_stream *response; + enum feerate_style *style; + u32 rate; + + if (!param(cmd, buffer, params, + p_req("style", param_feerate_style, &style), + NULL)) + return command_param_failed(); + + const size_t num_feerates = tal_count(ld->topology->feerates[0]); + + response = json_stream_success(cmd); + if (!num_feerates) + json_add_string(response, "warning_missing_feerates", + "Some fee estimates unavailable: bitcoind startup?"); + + json_object_start(response, feerate_style_name(*style)); + rate = opening_feerate(ld); + if (rate) + json_add_num(response, "opening", feerate_to_style(rate, *style)); + rate = mutual_close_feerate(ld); + if (rate) + json_add_num(response, "mutual_close", + feerate_to_style(rate, *style)); + rate = unilateral_feerate(ld, false); + if (rate) + json_add_num(response, "unilateral_close", + feerate_to_style(rate, *style)); + rate = unilateral_feerate(ld, true); + if (rate) + json_add_num(response, "unilateral_anchor_close", + feerate_to_style(rate, *style)); + rate = penalty_feerate(ld); + if (rate) + json_add_num(response, "penalty", + feerate_to_style(rate, *style)); + rate = unilateral_feerate(ld, true); + if (rate) { + rate += ld->config.feerate_offset; + if (rate > feerate_max(ld, NULL)) + rate = feerate_max(ld, NULL); + json_add_num(response, "splice", + feerate_to_style(rate, *style)); + } + + json_add_u64(response, "min_acceptable", + feerate_to_style(feerate_min(ld, NULL), *style)); + json_add_u64(response, "max_acceptable", + feerate_to_style(feerate_max(ld, NULL), *style)); + json_add_u64(response, "floor", + feerate_to_style(get_feerate_floor(ld), *style)); + + json_array_start(response, "estimates"); + assert(tal_count(ld->topology->smoothed_feerates) == num_feerates); + for (size_t i = 0; i < num_feerates; i++) { + json_object_start(response, NULL); + json_add_num(response, "blockcount", + ld->topology->feerates[0][i].blockcount); + json_add_u64(response, "feerate", + feerate_to_style(ld->topology->feerates[0][i].rate, *style)); + json_add_u64(response, "smoothed_feerate", + feerate_to_style(ld->topology->smoothed_feerates[i].rate, + *style)); + json_object_end(response); + } + json_array_end(response); + json_object_end(response); + + if (num_feerates) { + /* It actually is negotiated per-channel... */ + bool anchor_outputs + = feature_offered(ld->our_features->bits[INIT_FEATURE], + OPT_ANCHOR_OUTPUTS_DEPRECATED) + || feature_offered(ld->our_features->bits[INIT_FEATURE], + OPT_ANCHORS_ZERO_FEE_HTLC_TX); + + json_object_start(response, "onchain_fee_estimates"); + /* eg 020000000001016f51de645a47baa49a636b8ec974c28bdff0ac9151c0f4eda2dbe3b41dbe711d000000001716001401fad90abcd66697e2592164722de4a95ebee165ffffffff0240420f00000000002200205b8cd3b914cf67cdd8fa6273c930353dd36476734fbd962102c2df53b90880cdb73f890000000000160014c2ccab171c2a5be9dab52ec41b825863024c54660248304502210088f65e054dbc2d8f679de3e40150069854863efa4a45103b2bb63d060322f94702200d3ae8923924a458cffb0b7360179790830027bb6b29715ba03e12fc22365de1012103d745445c9362665f22e0d96e9e766f273f3260dea39c8a76bfa05dd2684ddccf00000000 == weight 702 */ + json_add_num(response, "opening_channel_satoshis", + opening_feerate(ld) * 702 / 1000); + /* eg. 02000000000101afcfac637d44d4e0df52031dba55b18d3f1bd79ad4b7ebbee964f124c5163dc30100000000ffffffff02400d03000000000016001427213e2217b4f56bd19b6c8393dc9f61be691233ca1f0c0000000000160014071c49cad2f420f3c805f9f6b98a57269cb1415004004830450221009a12b4d5ae1d41781f79bedecfa3e65542b1799a46c272287ba41f009d2e27ff0220382630c899207487eba28062f3989c4b656c697c23a8c89c1d115c98d82ff261014730440220191ddf13834aa08ea06dca8191422e85d217b065462d1b405b665eefa0684ed70220252409bf033eeab3aae89ae27596d7e0491bcc7ae759c5644bced71ef3cccef30147522102324266de8403b3ab157a09f1f784d587af61831c998c151bcc21bb74c2b2314b2102e3bd38009866c9da8ec4aa99cc4ea9c6c0dd46df15c61ef0ce1f271291714e5752ae00000000 == weight 673 */ + json_add_u64(response, "mutual_close_satoshis", + mutual_close_feerate(ld) * 673 / 1000); + /* eg. 02000000000101c4fecaae1ea940c15ec502de732c4c386d51f981317605bbe5ad2c59165690ab00000000009db0e280010a2d0f00000000002200208d290003cedb0dd00cd5004c2d565d55fc70227bf5711186f4fa9392f8f32b4a0400483045022100952fcf8c730c91cf66bcb742cd52f046c0db3694dc461e7599be330a22466d790220740738a6f9d9e1ae5c86452fa07b0d8dddc90f8bee4ded24a88fe4b7400089eb01483045022100db3002a93390fc15c193da57d6ce1020e82705e760a3aa935ebe864bd66dd8e8022062ee9c6aa7b88ff4580e2671900a339754116371d8f40eba15b798136a76cd150147522102324266de8403b3ab157a09f1f784d587af61831c998c151bcc21bb74c2b2314b2102e3bd38009866c9da8ec4aa99cc4ea9c6c0dd46df15c61ef0ce1f271291714e5752ae9a3ed620 == weight 598 */ + /* Or, with anchors: + * 02000000000101dc824e8e880f90f397a74f89022b4d58f8c36ebc4fffc238bd525bd11f5002a501000000009db0e280044a010000000000002200200e1a08b3da3bea6a7a77315f95afcd589fe799af46cf9bfb89523172814050e44a01000000000000220020be7935a77ca9ab70a4b8b1906825637767fed3c00824aa90c988983587d6848878e001000000000022002009fa3082e61ca0bd627915b53b0cb8afa467248fa4dc95141f78b96e9c98a8ed245a0d000000000022002091fb9e7843a03e66b4b1173482a0eb394f03a35aae4c28e8b4b1f575696bd793040047304402205c2ea9cf6f670e2f454c054f9aaca2d248763e258e44c71675c06135fd8f36cb02201b564f0e1b3f1ea19342f26e978a4981675da23042b4d392737636738c3514da0147304402205fcd2af5b724cbbf71dfa07bd14e8018ce22c08a019976dc03d0f545f848d0a702203652200350cadb464a70a09829d09227ed3da8c6b8ef5e3a59b5eefd056deaae0147522102324266de8403b3ab157a09f1f784d587af61831c998c151bcc21bb74c2b2314b2102e3bd38009866c9da8ec4aa99cc4ea9c6c0dd46df15c61ef0ce1f271291714e5752ae9b3ed620 1112 */ + if (anchor_outputs) + json_add_u64(response, "unilateral_close_satoshis", + unilateral_feerate(ld, true) * 1112 / 1000); + else + json_add_u64(response, "unilateral_close_satoshis", + unilateral_feerate(ld, false) * 598 / 1000); + json_add_u64(response, "unilateral_close_nonanchor_satoshis", + unilateral_feerate(ld, false) * 598 / 1000); + + json_add_u64(response, "htlc_timeout_satoshis", + htlc_timeout_fee(htlc_resolution_feerate(ld), + false, false).satoshis /* Raw: estimate */); + json_add_u64(response, "htlc_success_satoshis", + htlc_success_fee(htlc_resolution_feerate(ld), + false, false).satoshis /* Raw: estimate */); + json_object_end(response); + } + + return command_success(cmd, response); +} + +static const struct json_command feerates_command = { + "feerates", + json_feerates, +}; +AUTODATA(json_command, &feerates_command); + +static struct command_result *json_parse_feerate(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct json_stream *response; + u32 *feerate; + + if (!param(cmd, buffer, params, + p_req("feerate", param_feerate, &feerate), + NULL)) + return command_param_failed(); + + response = json_stream_success(cmd); + json_add_num(response, feerate_style_name(FEERATE_PER_KSIPA), + feerate_to_style(*feerate, FEERATE_PER_KSIPA)); + return command_success(cmd, response); +} + +static const struct json_command parse_feerate_command = { + "parsefeerate", + json_parse_feerate, +}; +AUTODATA(json_command, &parse_feerate_command); diff --git a/lightningd/feerate.h b/lightningd/feerate.h index ca0793d5b963..e05239b186f0 100644 --- a/lightningd/feerate.h +++ b/lightningd/feerate.h @@ -3,8 +3,16 @@ #include "config.h" #include #include +#include struct command; +struct lightningd; + +/* Our plugins give us a series of blockcount, feerate pairs. */ +struct feerate_est { + u32 blockcount; + u32 rate; +}; enum feerate { /* DO NOT REORDER: force-feerates uses this order! */ @@ -39,4 +47,45 @@ struct command_result *param_feerate(struct command *cmd, const char *name, const char *buffer, const jsmntok_t *tok, u32 **feerate); +/* Get the minimum feerate that bitcoind will accept */ +u32 get_feerate_floor(const struct lightningd *ld); + +/* Has our feerate estimation failed altogether? */ +bool unknown_feerates(const struct lightningd *ld); + +/* Get feerate estimate for getting a tx in this many blocks */ +u32 feerate_for_deadline(const struct lightningd *ld, u32 blockcount); +u32 smoothed_feerate_for_deadline(const struct lightningd *ld, u32 blockcount); + +/* Get feerate to hit this *block number*. */ +u32 feerate_for_target(const struct lightningd *ld, u64 deadline); + +/* Get range of feerates to insist other side abide by for normal channels. + * If we have to guess, sets *unknown to true, otherwise false. */ +u32 feerate_min(struct lightningd *ld, bool *unknown); +u32 feerate_max(struct lightningd *ld, bool *unknown); + +/* These return 0 if unknown */ +u32 opening_feerate(struct lightningd *ld); +u32 mutual_close_feerate(struct lightningd *ld); +u32 unilateral_feerate(struct lightningd *ld, bool option_anchors); +u32 delayed_to_us_feerate(struct lightningd *ld); +u32 htlc_resolution_feerate(struct lightningd *ld); +u32 penalty_feerate(struct lightningd *ld); + +/* Usually we set nLocktime to tip (or recent) like bitcoind does */ +u32 default_locktime(const struct lightningd *ld); + +/* Feed a fresh feerate sample into the smoothing/history machinery. */ +void update_feerates(struct lightningd *ld, + u32 feerate_floor, + const struct feerate_est *rates TAKES, + void *arg); + +/* Start polling bitcoind for fee estimates every 30s */ +void start_fee_polling(struct lightningd *ld); + +/* In channel_control.c */ +void notify_feerate_change(struct lightningd *ld); + #endif /* LIGHTNING_LIGHTNINGD_FEERATE_H */ diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c index 21486932ff1f..ed42de24c0f8 100644 --- a/lightningd/onchain_control.c +++ b/lightningd/onchain_control.c @@ -1021,7 +1021,7 @@ static struct bitcoin_tx *onchaind_tx_unsigned(const tal_t *ctx, for (;;) { u32 feerate; - feerate = feerate_for_target(ld->topology, block_target); + feerate = feerate_for_target(ld, block_target); *fee = amount_tx_fee(feerate, weight); log_debug(channel->log, @@ -1061,8 +1061,8 @@ static struct bitcoin_tx *onchaind_tx_unsigned(const tal_t *ctx, "Lowballing feerate for %s sats from %u to %u (deadline %u->%"PRIu64"):" " won't count on it being spent!", fmt_amount_sat(tmpctx, info->out_sats), - feerate_for_target(ld->topology, info->deadline_block), - feerate_for_target(ld->topology, block_target), + feerate_for_target(ld, info->deadline_block), + feerate_for_target(ld, block_target), info->deadline_block, block_target); } } @@ -1193,7 +1193,7 @@ static bool consider_onchain_htlc_tx_rebroadcast(struct channel *channel, * but since that bitcoind will take the highest feerate ones, it will * priority order them for us. */ - feerate = feerate_for_target(ld->topology, info->deadline_block); + feerate = feerate_for_target(ld, info->deadline_block); /* Make a copy to play with */ newtx = clone_bitcoin_tx(tmpctx, info->raw_htlc_tx); diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index d7998197d3d4..1016cd7dda93 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -874,7 +874,7 @@ static void opening_got_offer(struct subd *openingd, /* Don't allow opening if we don't know any fees; even if * ignore-feerates is set. */ - if (unknown_feerates(openingd->ld->topology)) { + if (unknown_feerates(openingd->ld)) { subd_send_msg(openingd, take(towire_openingd_got_offer_reply(NULL, "Cannot accept channel: feerates unknown", NULL, NULL, NULL, 0))); @@ -1346,14 +1346,14 @@ static struct command_result *json_fundchannel_start(struct command *cmd, * money in the immediate-close case, which is probably soon * and thus current feerates are sufficient. */ feerate_non_anchor = tal(cmd, u32); - *feerate_non_anchor = opening_feerate(cmd->ld->topology); + *feerate_non_anchor = opening_feerate(cmd->ld); if (!*feerate_non_anchor) { return command_fail(cmd, LIGHTNINGD, "Cannot estimate fees"); } } - feerate_anchor = unilateral_feerate(cmd->ld->topology, true); + feerate_anchor = unilateral_feerate(cmd->ld, true); /* Only complain here if we could possibly open one! */ if (!feerate_anchor && feature_offered(cmd->ld->our_features->bits[INIT_FEATURE], @@ -1362,10 +1362,10 @@ static struct command_result *json_fundchannel_start(struct command *cmd, "Cannot estimate fees"); } - if (*feerate_non_anchor < get_feerate_floor(cmd->ld->topology)) { + if (*feerate_non_anchor < get_feerate_floor(cmd->ld)) { return command_fail(cmd, LIGHTNINGD, "Feerate for non-anchor (%u perkw) below feerate floor %u perkw", - *feerate_non_anchor, get_feerate_floor(cmd->ld->topology)); + *feerate_non_anchor, get_feerate_floor(cmd->ld)); } peer = peer_by_id(cmd->ld, id); diff --git a/lightningd/test/run-jsonrpc.c b/lightningd/test/run-jsonrpc.c index 611cc04825e0..3e2ca8800c49 100644 --- a/lightningd/test/run-jsonrpc.c +++ b/lightningd/test/run-jsonrpc.c @@ -8,6 +8,15 @@ #include /* AUTOGENERATED MOCKS START */ +/* Generated stub for bitcoind_estimate_fees_ */ +void bitcoind_estimate_fees_(const tal_t *ctx UNNEEDED, + struct bitcoind *bitcoind UNNEEDED, + void (*cb)(struct lightningd *ld UNNEEDED, + u32 feerate_floor UNNEEDED, + const struct feerate_est *feerates UNNEEDED, + void *arg) UNNEEDED, + void *cb_arg UNNEEDED) +{ fprintf(stderr, "bitcoind_estimate_fees_ called!\n"); abort(); } /* Generated stub for db_begin_transaction_ */ void db_begin_transaction_(struct db *db UNNEEDED, const char *location UNNEEDED) { fprintf(stderr, "db_begin_transaction_ called!\n"); abort(); } @@ -23,12 +32,9 @@ void db_set_readonly(struct db *db UNNEEDED, bool readonly UNNEEDED) /* Generated stub for fatal */ void fatal(const char *fmt UNNEEDED, ...) { fprintf(stderr, "fatal called!\n"); abort(); } -/* Generated stub for feerate_for_deadline */ -u32 feerate_for_deadline(const struct chain_topology *topo UNNEEDED, u32 blockcount UNNEEDED) -{ fprintf(stderr, "feerate_for_deadline called!\n"); abort(); } -/* Generated stub for get_feerate_floor */ -u32 get_feerate_floor(const struct chain_topology *topo UNNEEDED) -{ fprintf(stderr, "get_feerate_floor called!\n"); abort(); } +/* Generated stub for get_block_height */ +u32 get_block_height(const struct chain_topology *topo UNNEEDED) +{ fprintf(stderr, "get_block_height called!\n"); abort(); } /* Generated stub for hsm_secret_arg */ char *hsm_secret_arg(const tal_t *ctx UNNEEDED, const char *arg UNNEEDED, @@ -65,20 +71,14 @@ void log_io(struct logger *logger UNNEEDED, enum log_level dir UNNEEDED, const char *comment UNNEEDED, const void *data UNNEEDED, size_t len UNNEEDED) { fprintf(stderr, "log_io called!\n"); abort(); } -/* Generated stub for mutual_close_feerate */ -u32 mutual_close_feerate(struct chain_topology *topo UNNEEDED) -{ fprintf(stderr, "mutual_close_feerate called!\n"); abort(); } /* Generated stub for new_logger */ struct logger *new_logger(const tal_t *ctx UNNEEDED, struct log_book *record UNNEEDED, const struct node_id *default_node_id UNNEEDED, const char *fmt UNNEEDED, ...) { fprintf(stderr, "new_logger called!\n"); abort(); } -/* Generated stub for opening_feerate */ -u32 opening_feerate(struct chain_topology *topo UNNEEDED) -{ fprintf(stderr, "opening_feerate called!\n"); abort(); } -/* Generated stub for penalty_feerate */ -u32 penalty_feerate(struct chain_topology *topo UNNEEDED) -{ fprintf(stderr, "penalty_feerate called!\n"); abort(); } +/* Generated stub for notify_feerate_change */ +void notify_feerate_change(struct lightningd *ld UNNEEDED) +{ fprintf(stderr, "notify_feerate_change called!\n"); abort(); } /* Generated stub for plugin_hook_call_ */ bool plugin_hook_call_(struct lightningd *ld UNNEEDED, struct plugin_hook *hook UNNEEDED, @@ -87,9 +87,6 @@ bool plugin_hook_call_(struct lightningd *ld UNNEEDED, const char *cmd_id TAKES UNNEEDED, tal_t *cb_arg STEALS UNNEEDED) { fprintf(stderr, "plugin_hook_call_ called!\n"); abort(); } -/* Generated stub for unilateral_feerate */ -u32 unilateral_feerate(struct chain_topology *topo UNNEEDED, bool option_anchors UNNEEDED) -{ fprintf(stderr, "unilateral_feerate called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ static int test_json_filter(void) diff --git a/wallet/reservation.c b/wallet/reservation.c index a340cd8803e7..c28a52a875a5 100644 --- a/wallet/reservation.c +++ b/wallet/reservation.c @@ -348,7 +348,7 @@ static struct command_result *finish_psbt(struct command *cmd, if (!locktime) { locktime = tal(cmd, u32); - *locktime = default_locktime(cmd->ld->topology); + *locktime = default_locktime(cmd->ld); } psbt = psbt_using_utxos(cmd, cmd->ld->wallet, utxos, @@ -675,7 +675,7 @@ static struct command_result *json_addpsbtoutput(struct command *cmd, if (!psbt) { if (!locktime) { locktime = tal(cmd, u32); - *locktime = default_locktime(cmd->ld->topology); + *locktime = default_locktime(cmd->ld); } psbt = create_psbt(cmd, 0, 0, *locktime); } else if (locktime) { @@ -785,7 +785,7 @@ static struct command_result *json_addpsbtinput(struct command *cmd, if (!psbt) { if (!locktime) { locktime = tal(cmd, u32); - *locktime = default_locktime(cmd->ld->topology); + *locktime = default_locktime(cmd->ld); } psbt = create_psbt(cmd, 0, 0, *locktime); } else if (locktime) { @@ -805,7 +805,7 @@ static struct command_result *json_addpsbtinput(struct command *cmd, if (!min_feerate) { min_feerate = tal(cmd, u32); - *min_feerate = opening_feerate(cmd->ld->topology); + *min_feerate = opening_feerate(cmd->ld); } all = amount_sat_eq(*req_amount, AMOUNT_SAT(-1ULL)); From fa8790c836ff314b8d2457453d916d13f7513ddf Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 15:10:10 +0930 Subject: [PATCH 73/77] lightningd: move feerate state to watchman The feerate samples now live alongside everything else watchman owns; chain_topology no longer needs to know about them. --- lightningd/chaintopology.c | 2 - lightningd/chaintopology.h | 13 ------- lightningd/feerate.c | 75 +++++++++++++++++++------------------- lightningd/feerate.h | 3 ++ lightningd/watchman.c | 3 ++ lightningd/watchman.h | 11 ++++++ 6 files changed, 55 insertions(+), 52 deletions(-) diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index 5816c58b17dd..3c9decf163cb 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -521,8 +521,6 @@ struct chain_topology *new_topology(struct lightningd *ld, struct logger *log) topo->blockdepthwatches = new_htable(topo, blockdepthwatch_hash); topo->log = log; topo->poll_seconds = 30; - memset(topo->feerates, 0, sizeof(topo->feerates)); - topo->smoothed_feerates = NULL; topo->root = NULL; topo->sync_waiters = tal(topo, struct list_head); topo->extend_timer = NULL; diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index 99215464f87e..47fc6e25d517 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -14,9 +14,6 @@ struct txwatch; struct scriptpubkeywatch; struct wallet; -/* We keep the last three in case there are outliers (for min/max) */ -#define FEE_HISTORY_NUM 3 - struct block { u32 height; @@ -65,16 +62,6 @@ struct chain_topology { struct bitcoin_blkid prev_tip; struct block_map *block_map; - /* This is the lowest feerate that bitcoind is saying will broadcast. */ - u32 feerate_floor; - - /* We keep last three feerates we got: this is useful for min/max. */ - struct feerate_est *feerates[FEE_HISTORY_NUM]; - - /* We keep a smoothed feerate: this is useful when we're going to - * suggest feerates / check feerates from our peers. */ - struct feerate_est *smoothed_feerates; - /* Where to log things. */ struct logger *log; diff --git a/lightningd/feerate.c b/lightningd/feerate.c index fb43d464bbf9..ea17b611642e 100644 --- a/lightningd/feerate.c +++ b/lightningd/feerate.c @@ -17,6 +17,7 @@ #include #include #include +#include #include const char *feerate_name(enum feerate feerate) @@ -183,7 +184,7 @@ static void next_updatefee_timer(struct lightningd *ld); bool unknown_feerates(const struct lightningd *ld) { - return tal_count(ld->topology->feerates[0]) == 0; + return tal_count(ld->watchman->feerates[0]) == 0; } static u32 interp_feerate(const struct feerate_est *rates, u32 blockcount) @@ -227,11 +228,11 @@ static u32 interp_feerate(const struct feerate_est *rates, u32 blockcount) u32 feerate_for_deadline(const struct lightningd *ld, u32 blockcount) { - u32 rate = interp_feerate(ld->topology->feerates[0], blockcount); + u32 rate = interp_feerate(ld->watchman->feerates[0], blockcount); /* 0 is a special value, meaning "don't know" */ - if (rate && rate < ld->topology->feerate_floor) - rate = ld->topology->feerate_floor; + if (rate && rate < ld->watchman->feerate_floor) + rate = ld->watchman->feerate_floor; return rate; } @@ -239,7 +240,7 @@ u32 smoothed_feerate_for_deadline(const struct lightningd *ld, u32 blockcount) { /* Note: we cap it at feerate_floor when we smooth */ - return interp_feerate(ld->topology->smoothed_feerates, blockcount); + return interp_feerate(ld->watchman->smoothed_feerates, blockcount); } /* feerate_for_deadline, but really lowball for distant targets */ @@ -342,9 +343,9 @@ void update_feerates(struct lightningd *ld, { struct feerate_est *new_smoothed; bool changed; - struct chain_topology *topo = ld->topology; + struct watchman *wm = ld->watchman; - topo->feerate_floor = feerate_floor; + wm->feerate_floor = feerate_floor; /* Don't bother updating if we got no feerates; we'd rather have * historical ones, if any. */ @@ -352,31 +353,31 @@ void update_feerates(struct lightningd *ld, return; /* If the feerate blockcounts differ, don't average, just override */ - if (topo->feerates[0] && different_blockcounts(ld, topo->feerates[0], rates)) { - for (size_t i = 0; i < ARRAY_SIZE(topo->feerates); i++) - topo->feerates[i] = tal_free(topo->feerates[i]); - topo->smoothed_feerates = tal_free(topo->smoothed_feerates); + if (wm->feerates[0] && different_blockcounts(ld, wm->feerates[0], rates)) { + for (size_t i = 0; i < ARRAY_SIZE(wm->feerates); i++) + wm->feerates[i] = tal_free(wm->feerates[i]); + wm->smoothed_feerates = tal_free(wm->smoothed_feerates); } /* Move down historical rates, insert these */ - tal_free(topo->feerates[FEE_HISTORY_NUM-1]); - memmove(topo->feerates + 1, topo->feerates, - sizeof(topo->feerates[0]) * (FEE_HISTORY_NUM-1)); - topo->feerates[0] = tal_dup_talarr(topo, struct feerate_est, rates); - changed = feerates_differ(topo->feerates[0], topo->feerates[1]); + tal_free(wm->feerates[FEE_HISTORY_NUM-1]); + memmove(wm->feerates + 1, wm->feerates, + sizeof(wm->feerates[0]) * (FEE_HISTORY_NUM-1)); + wm->feerates[0] = tal_dup_talarr(wm, struct feerate_est, rates); + changed = feerates_differ(wm->feerates[0], wm->feerates[1]); /* Use this as basis of new smoothed ones. */ - new_smoothed = tal_dup_talarr(topo, struct feerate_est, topo->feerates[0]); + new_smoothed = tal_dup_talarr(wm, struct feerate_est, wm->feerates[0]); /* If there were old smoothed feerates, incorporate those */ - if (tal_count(topo->smoothed_feerates) != 0) { + if (tal_count(wm->smoothed_feerates) != 0) { const size_t num_new = tal_count(new_smoothed); for (size_t i = 0; i < num_new; i++) smooth_one_feerate(ld, &new_smoothed[i]); } - changed |= feerates_differ(topo->smoothed_feerates, new_smoothed); - tal_free(topo->smoothed_feerates); - topo->smoothed_feerates = new_smoothed; + changed |= feerates_differ(wm->smoothed_feerates, new_smoothed); + tal_free(wm->smoothed_feerates); + wm->smoothed_feerates = new_smoothed; if (changed) notify_feerate_change(ld); @@ -489,12 +490,12 @@ u32 penalty_feerate(struct lightningd *ld) u32 get_feerate_floor(const struct lightningd *ld) { - return ld->topology->feerate_floor; + return ld->watchman->feerate_floor; } u32 feerate_min(struct lightningd *ld, bool *unknown) { - const struct chain_topology *topo = ld->topology; + const struct watchman *wm = ld->watchman; u32 min; if (unknown) @@ -514,11 +515,11 @@ u32 feerate_min(struct lightningd *ld, bool *unknown) * [1] https://github.com/ElementsProject/lightning/issues/6362 * */ min = 0xFFFFFFFF; - for (size_t i = 0; i < ARRAY_SIZE(topo->feerates); i++) { - const size_t num_feerates = tal_count(topo->feerates[i]); + for (size_t i = 0; i < ARRAY_SIZE(wm->feerates); i++) { + const size_t num_feerates = tal_count(wm->feerates[i]); for (size_t j = 0; j < num_feerates; j++) { - if (topo->feerates[i][j].rate < min) - min = topo->feerates[i][j].rate; + if (wm->feerates[i][j].rate < min) + min = wm->feerates[i][j].rate; } } if (min == 0xFFFFFFFF) { @@ -538,17 +539,17 @@ u32 feerate_min(struct lightningd *ld, bool *unknown) u32 feerate_max(struct lightningd *ld, bool *unknown) { - const struct chain_topology *topo = ld->topology; + const struct watchman *wm = ld->watchman; u32 max = 0; if (unknown) *unknown = false; - for (size_t i = 0; i < ARRAY_SIZE(topo->feerates); i++) { - const size_t num_feerates = tal_count(topo->feerates[i]); + for (size_t i = 0; i < ARRAY_SIZE(wm->feerates); i++) { + const size_t num_feerates = tal_count(wm->feerates[i]); for (size_t j = 0; j < num_feerates; j++) { - if (topo->feerates[i][j].rate > max) - max = topo->feerates[i][j].rate; + if (wm->feerates[i][j].rate > max) + max = wm->feerates[i][j].rate; } } if (!max) { @@ -596,7 +597,7 @@ static struct command_result *json_feerates(struct command *cmd, NULL)) return command_param_failed(); - const size_t num_feerates = tal_count(ld->topology->feerates[0]); + const size_t num_feerates = tal_count(ld->watchman->feerates[0]); response = json_stream_success(cmd); if (!num_feerates) @@ -640,15 +641,15 @@ static struct command_result *json_feerates(struct command *cmd, feerate_to_style(get_feerate_floor(ld), *style)); json_array_start(response, "estimates"); - assert(tal_count(ld->topology->smoothed_feerates) == num_feerates); + assert(tal_count(ld->watchman->smoothed_feerates) == num_feerates); for (size_t i = 0; i < num_feerates; i++) { json_object_start(response, NULL); json_add_num(response, "blockcount", - ld->topology->feerates[0][i].blockcount); + ld->watchman->feerates[0][i].blockcount); json_add_u64(response, "feerate", - feerate_to_style(ld->topology->feerates[0][i].rate, *style)); + feerate_to_style(ld->watchman->feerates[0][i].rate, *style)); json_add_u64(response, "smoothed_feerate", - feerate_to_style(ld->topology->smoothed_feerates[i].rate, + feerate_to_style(ld->watchman->smoothed_feerates[i].rate, *style)); json_object_end(response); } diff --git a/lightningd/feerate.h b/lightningd/feerate.h index e05239b186f0..e1bedbe062c6 100644 --- a/lightningd/feerate.h +++ b/lightningd/feerate.h @@ -8,6 +8,9 @@ struct command; struct lightningd; +/* We keep the last three in case there are outliers (for min/max) */ +#define FEE_HISTORY_NUM 3 + /* Our plugins give us a series of blockcount, feerate pairs. */ struct feerate_est { u32 blockcount; diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 84b21d427676..55c54d2eb341 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -166,6 +166,9 @@ struct watchman *watchman_new(const tal_t *ctx, struct lightningd *ld) wm->ld = ld; wm->pending_ops = tal_arr(wm, struct pending_op *, 0); + wm->feerate_floor = 0; + memset(wm->feerates, 0, sizeof(wm->feerates)); + wm->smoothed_feerates = NULL; load_pending_ops(wm); load_tip(wm); diff --git a/lightningd/watchman.h b/lightningd/watchman.h index e1c77d64f525..ca23d262a9a6 100644 --- a/lightningd/watchman.h +++ b/lightningd/watchman.h @@ -8,6 +8,7 @@ #include #include #include +#include struct lightningd; struct pending_op; @@ -22,6 +23,16 @@ struct watchman { struct bitcoin_blkid last_processed_hash; u32 bitcoind_blockcount; struct pending_op **pending_ops; + + /* Lowest feerate bitcoind says it will broadcast. */ + u32 feerate_floor; + + /* Last three feerate samples (for min/max windows). */ + struct feerate_est *feerates[FEE_HISTORY_NUM]; + + /* Exponentially smoothed feerate: used when proposing/checking + * feerates with peers. */ + struct feerate_est *smoothed_feerates; }; /** From b81bc4719286c0e6a706065e3268d26e2e9e3e34 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Wed, 22 Apr 2026 15:29:58 +0930 Subject: [PATCH 74/77] lightningd: drop poll_seconds and request_ctx from chain_topology Part of the chaintopology spring clean. Wrap the fee poll in a small struct fee_poll on lightningd, hardcode the cadence at 30s (BITCOIND_POLL_SECONDS, matching bwatch's default), and use topo as the bcli request ctx. The fee poll is a stopgap; eventually bwatch will push feerate updates alongside blocks. --dev-bitcoind-poll is kept as a deprecated no-op so existing test fixtures keep parsing. --- lightningd/chaintopology.c | 20 +++++------- lightningd/chaintopology.h | 8 +---- lightningd/feerate.c | 64 +++++++++++++++++++++----------------- lightningd/feerate.h | 7 +++-- lightningd/lightningd.c | 2 ++ lightningd/lightningd.h | 5 +++ lightningd/options.c | 4 +-- 7 files changed, 58 insertions(+), 52 deletions(-) diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index 3c9decf163cb..b21997fe17d4 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -27,7 +27,7 @@ static void next_topology_timer(struct chain_topology *topo) { assert(!topo->extend_timer); topo->extend_timer = new_reltimer(topo->ld->timers, topo, - time_from_sec(topo->poll_seconds), + time_from_sec(BITCOIND_POLL_SECONDS), try_extend_tip, topo); } @@ -485,7 +485,7 @@ static void try_extend_tip(struct chain_topology *topo) { topo->extend_timer = NULL; trace_span_start("extend_tip", topo); - bitcoind_getrawblockbyheight(topo->request_ctx, topo->ld->bitcoind, topo->tip->height + 1, + bitcoind_getrawblockbyheight(topo, topo->ld->bitcoind, topo->tip->height + 1, get_new_block, topo); } @@ -520,13 +520,10 @@ struct chain_topology *new_topology(struct lightningd *ld, struct logger *log) topo->scriptpubkeywatches = new_htable(topo, scriptpubkeywatch_hash); topo->blockdepthwatches = new_htable(topo, blockdepthwatch_hash); topo->log = log; - topo->poll_seconds = 30; topo->root = NULL; topo->sync_waiters = tal(topo, struct list_head); topo->extend_timer = NULL; - topo->updatefee_timer = NULL; topo->checkchain_timer = NULL; - topo->request_ctx = tal(topo, char); list_head_init(topo->sync_waiters); return topo; @@ -577,15 +574,14 @@ static void retry_sync_getchaininfo_done(struct bitcoind *bitcoind, const char * topo->checkchain_timer = new_reltimer(bitcoind->ld->timers, topo, /* Be 4x more aggressive in this case. */ - time_divide(time_from_sec(bitcoind->ld->topology - ->poll_seconds), 4), + time_divide(time_from_sec(BITCOIND_POLL_SECONDS), 4), retry_sync, topo); } static void retry_sync(struct chain_topology *topo) { topo->checkchain_timer = NULL; - bitcoind_getchaininfo(topo->request_ctx, topo->ld->bitcoind, get_block_height(topo), + bitcoind_getchaininfo(topo, topo->ld->bitcoind, get_block_height(topo), retry_sync_getchaininfo_done, topo); } @@ -772,7 +768,7 @@ void setup_topology(struct chain_topology *topo) chaininfo->ibd, topo, true); /* It's very useful to have feerates early */ - update_feerates(topo->ld, feerates->feerate_floor, feerates->rates, NULL); + update_feerates(topo->ld, feerates->feerate_floor, feerates->rates); /* Get the first block, so we can initialize topography. */ bitcoind_getrawblockbyheight(topo, topo->ld->bitcoind, blockscan_start, @@ -867,8 +863,6 @@ void stop_topology(struct chain_topology *topo) /* Remove timers while we're cleaning up plugins. */ tal_free(topo->checkchain_timer); tal_free(topo->extend_timer); - tal_free(topo->updatefee_timer); - - /* Don't handle responses to any existing requests. */ - tal_free(topo->request_ctx); + tal_free(topo->ld->fee_poll); + topo->ld->fee_poll = NULL; } diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index 47fc6e25d517..d9799a50e108 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -65,19 +65,13 @@ struct chain_topology { /* Where to log things. */ struct logger *log; - /* How often to poll. */ - u32 poll_seconds; - /* struct sync_waiters waiting for us to catch up with bitcoind (and * once that has caught up with the network). NULL if we're already * caught up. */ struct list_head *sync_waiters; /* Timers we're running. */ - struct oneshot *checkchain_timer, *extend_timer, *updatefee_timer; - - /* Parent context for requests (to bcli plugin) we have outstanding. */ - tal_t *request_ctx; + struct oneshot *checkchain_timer, *extend_timer; /* Transactions/txos we are watching. */ struct txwatch_hash *txwatches; diff --git a/lightningd/feerate.c b/lightningd/feerate.c index ea17b611642e..0b81a2f59904 100644 --- a/lightningd/feerate.c +++ b/lightningd/feerate.c @@ -180,7 +180,16 @@ struct command_result *param_feerate_val(struct command *cmd, } /* Mutual recursion via timer. */ -static void next_updatefee_timer(struct lightningd *ld); +/* Fee polling: lightningd polls bitcoind for fee estimates every 30 seconds. + * bwatch only reports blockheight via block_processed; it does not call + * estimatefees. */ +struct fee_poll { + struct lightningd *ld; + struct oneshot *timer; +}; + +static void start_fee_estimate(struct fee_poll *fp); +static void schedule_fee_estimate(struct fee_poll *fp); bool unknown_feerates(const struct lightningd *ld) { @@ -272,9 +281,8 @@ static void smooth_one_feerate(const struct lightningd *ld, { /* Smoothing factor alpha for simple exponential smoothing. The goal is to * have the feerate account for 90 percent of the values polled in the last - * 2 minutes. The following will do that in a polling interval - * independent manner. */ - double alpha = 1 - pow(0.1,(double)ld->topology->poll_seconds / 120); + * 2 minutes. */ + double alpha = 1 - pow(0.1, (double)BITCOIND_POLL_SECONDS / 120); u32 old_feerate, feerate_smooth; /* We don't call this unless we had a previous feerate */ @@ -292,7 +300,7 @@ static void smooth_one_feerate(const struct lightningd *ld, rate->rate = get_feerate_floor(ld); if (rate->rate != feerate_smooth) - log_debug(ld->topology->log, + log_debug(ld->log, "Feerate estimate for %u blocks set to %u (was %u)", rate->blockcount, rate->rate, feerate_smooth); } @@ -319,14 +327,14 @@ static bool different_blockcounts(struct lightningd *ld, { const size_t num_feerates = tal_count(old); if (num_feerates != tal_count(new)) { - log_unusual(ld->topology->log, + log_unusual(ld->log, "Presented with %zu feerates this time (was %zu!)", tal_count(new), num_feerates); return true; } for (size_t i = 0; i < num_feerates; i++) { if (old[i].blockcount != new[i].blockcount) { - log_unusual(ld->topology->log, + log_unusual(ld->log, "Presented with feerates" " for blockcount %u, previously %u", new[i].blockcount, old[i].blockcount); @@ -338,8 +346,7 @@ static bool different_blockcounts(struct lightningd *ld, void update_feerates(struct lightningd *ld, u32 feerate_floor, - const struct feerate_est *rates TAKES, - void *arg UNUSED) + const struct feerate_est *rates TAKES) { struct feerate_est *new_smoothed; bool changed; @@ -383,35 +390,36 @@ void update_feerates(struct lightningd *ld, notify_feerate_change(ld); } -static void update_feerates_repeat(struct lightningd *ld, - u32 feerate_floor, - const struct feerate_est *rates TAKES, - void *unused) +static void update_feerates_and_reschedule(struct lightningd *ld, + u32 feerate_floor, + const struct feerate_est *rates TAKES, + struct fee_poll *fp) { - update_feerates(ld, feerate_floor, rates, unused); - next_updatefee_timer(ld); + update_feerates(ld, feerate_floor, rates); + schedule_fee_estimate(fp); } -static void start_fee_estimate(struct lightningd *ld) +static void start_fee_estimate(struct fee_poll *fp) { - ld->topology->updatefee_timer = NULL; - /* Based on timer, update fee estimates. */ - bitcoind_estimate_fees(ld->topology->request_ctx, ld->bitcoind, - update_feerates_repeat, NULL); + fp->timer = NULL; + bitcoind_estimate_fees(fp, fp->ld->bitcoind, + update_feerates_and_reschedule, fp); } -void start_fee_polling(struct lightningd *ld) +static void schedule_fee_estimate(struct fee_poll *fp) { - start_fee_estimate(ld); + fp->timer = new_reltimer(fp->ld->timers, fp, + time_from_sec(BITCOIND_POLL_SECONDS), + start_fee_estimate, fp); } -static void next_updatefee_timer(struct lightningd *ld) +void start_fee_polling(struct lightningd *ld) { - assert(!ld->topology->updatefee_timer); - ld->topology->updatefee_timer - = new_reltimer(ld->timers, ld, - time_from_sec(ld->topology->poll_seconds), - start_fee_estimate, ld); + struct fee_poll *fp = tal(ld, struct fee_poll); + fp->ld = ld; + fp->timer = NULL; + ld->fee_poll = fp; + start_fee_estimate(fp); } struct rate_conversion { diff --git a/lightningd/feerate.h b/lightningd/feerate.h index e1bedbe062c6..6ea46f09bb6d 100644 --- a/lightningd/feerate.h +++ b/lightningd/feerate.h @@ -11,6 +11,10 @@ struct lightningd; /* We keep the last three in case there are outliers (for min/max) */ #define FEE_HISTORY_NUM 3 +/* How often we poll bitcoind (block extension + fee estimates). Matches + * bwatch's default chain poll cadence (bwatch-poll-interval = 30000ms). */ +#define BITCOIND_POLL_SECONDS 30 + /* Our plugins give us a series of blockcount, feerate pairs. */ struct feerate_est { u32 blockcount; @@ -82,8 +86,7 @@ u32 default_locktime(const struct lightningd *ld); /* Feed a fresh feerate sample into the smoothing/history machinery. */ void update_feerates(struct lightningd *ld, u32 feerate_floor, - const struct feerate_est *rates TAKES, - void *arg); + const struct feerate_est *rates TAKES); /* Start polling bitcoind for fee estimates every 30s */ void start_fee_polling(struct lightningd *ld); diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 2c0dcf141d0c..4660fbe643c4 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -278,6 +278,8 @@ static struct lightningd *new_lightningd(const tal_t *ctx) ld->bitcoind = new_bitcoind(ld, ld, ld->log); ld->outgoing_txs = new_htable(ld, outgoing_tx_map); ld->rebroadcast_timer = NULL; + ld->fee_poll = NULL; + ld->dev_bitcoind_poll_ignored = 0; ld->gossip_blockheight = 0; ld->daemon_parent_fd = -1; ld->proxyaddr = NULL; diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h index 5175925ebe8d..ce1cb80edeb3 100644 --- a/lightningd/lightningd.h +++ b/lightningd/lightningd.h @@ -13,6 +13,7 @@ struct amount_msat; struct bitcoind; +struct fee_poll; struct oneshot; struct outgoing_tx_map; struct watchman; @@ -251,6 +252,10 @@ struct lightningd { struct ext_key *bip86_base; struct wallet *wallet; struct watchman *watchman; + struct fee_poll *fee_poll; + + /* Deprecated --dev-bitcoind-poll value, ignored (bwatch drives updates). */ + u32 dev_bitcoind_poll_ignored; /* Outstanding waitsendpay commands. */ struct list_head waitsendpay_commands; diff --git a/lightningd/options.c b/lightningd/options.c index b724b200a089..dd3b68efa4a2 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -814,8 +814,8 @@ static void dev_register_opts(struct lightningd *ld) "Announce and allow announcments for localhost address"); clnopt_witharg("--dev-bitcoind-poll", OPT_DEV|OPT_SHOWINT, opt_set_u32, opt_show_u32, - &ld->topology->poll_seconds, - "Time between polling for new transactions"); + &ld->dev_bitcoind_poll_ignored, + "Deprecated: ignored (bwatch drives chain updates)"); clnopt_noarg("--dev-fast-gossip", OPT_DEV, opt_set_bool, &ld->dev_fast_gossip, From 0ef47fb2702cd7ef2653590e3910e7d2780cede3 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 09:14:33 +0930 Subject: [PATCH 75/77] lightningd: drop legacy unconfirmed-tx + splice-after-coopclose watches --- lightningd/chaintopology.c | 101 ------------------------------------- lightningd/chaintopology.h | 6 --- lightningd/peer_control.c | 36 ------------- wallet/walletrpc.c | 9 ++-- 4 files changed, 3 insertions(+), 149 deletions(-) diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index b21997fe17d4..c970cd01b81b 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -105,96 +105,6 @@ size_t get_tx_depth(const struct chain_topology *topo, return topo->tip->height - blockheight + 1; } -static enum watch_result closeinfo_txid_confirmed(struct lightningd *ld, - const struct bitcoin_txid *txid, - const struct bitcoin_tx *tx, - unsigned int depth, - void *unused) -{ - /* Sanity check. */ - if (tx != NULL) { - struct bitcoin_txid txid2; - - bitcoin_txid(tx, &txid2); - if (!bitcoin_txid_eq(txid, &txid2)) { - fatal("Txid for %s is not %s", - fmt_bitcoin_tx(tmpctx, tx), - fmt_bitcoin_txid(tmpctx, txid)); - } - } - - /* We delete ourselves first time, so should not be reorged out!! */ - assert(depth > 0); - /* Subtle: depth 1 == current block. */ - wallet_confirm_tx(ld->wallet, txid, - get_block_height(ld->topology) + 1 - depth); - return DELETE_WATCH; -} - -/* We need to know if close_info UTXOs (which the wallet doesn't natively know - * how to spend, so is not in the normal path) get reconfirmed. - * - * This can happen on startup (where we manually unwind 100 blocks) or on a - * reorg. The db NULLs out the confirmation_height, so we can't easily figure - * out just the new ones (and removing the ON DELETE SET NULL clause is - * non-trivial). - * - * So every time, we just set a notification for every tx in this class we're - * not already watching: there are not usually many, nor many reorgs, so the - * redundancy is OK. - */ -static void watch_for_utxo_reconfirmation(struct chain_topology *topo, - struct wallet *wallet) -{ - struct utxo **unconfirmed; - - unconfirmed = wallet_get_unconfirmed_closeinfo_utxos(tmpctx, wallet); - const size_t num_unconfirmed = tal_count(unconfirmed); - for (size_t i = 0; i < num_unconfirmed; i++) { - assert(unconfirmed[i]->close_info != NULL); - assert(unconfirmed[i]->blockheight == NULL); - - if (find_txwatch(topo, &unconfirmed[i]->outpoint.txid, - closeinfo_txid_confirmed, NULL)) - continue; - - watch_txid(topo, topo, - &unconfirmed[i]->outpoint.txid, - closeinfo_txid_confirmed, NULL); - } -} - -static enum watch_result tx_confirmed(struct lightningd *ld, - const struct bitcoin_txid *txid, - const struct bitcoin_tx *tx, - unsigned int depth, - void *unused) -{ - /* We don't actually need to do anything here: the fact that we were - * watching the tx made chaintopology.c update the transaction depth */ - if (depth != 0) - return DELETE_WATCH; - return KEEP_WATCHING; -} - -void watch_unconfirmed_txid(struct lightningd *ld, - struct chain_topology *topo, - const struct bitcoin_txid *txid) -{ - watch_txid(ld->wallet, topo, txid, tx_confirmed, NULL); -} - -static void watch_for_unconfirmed_txs(struct lightningd *ld, - struct chain_topology *topo) -{ - struct bitcoin_txid *txids; - - txids = wallet_transactions_by_height(tmpctx, ld->wallet, 0); - log_debug(ld->log, "Got %zu unconfirmed transactions", tal_count(txids)); - for (size_t i = 0; i < tal_count(txids); i++) - watch_unconfirmed_txid(ld, topo, &txids[i]); -} - struct sync_waiter { /* Linked from chain_topology->sync_waiters */ struct list_node list; @@ -434,9 +344,6 @@ static void remove_tip(struct chain_topology *topo) b->height); wallet_block_remove(topo->ld->wallet, b); - /* This may have unconfirmed txs: reconfirm as we add blocks. */ - watch_for_utxo_reconfirmation(topo, topo->ld->wallet); - /* Anyone watching for block removes */ watch_check_block_removed(topo, b->height); @@ -791,14 +698,6 @@ void setup_topology(struct chain_topology *topo) /* Rollback to the given blockheight, so we start track * correctly again */ wallet_blocks_rollback(topo->ld->wallet, blockscan_start); - - /* May have unconfirmed txs: reconfirm as we add blocks. */ - watch_for_utxo_reconfirmation(topo, topo->ld->wallet); - - /* We usually watch txs because we have outputs coming to us, or they're - * related to a channel. But not if they're created by sendpsbt without any - * outputs to us. */ - watch_for_unconfirmed_txs(topo->ld, topo); db_commit_transaction(topo->ld->wallet->db); tal_free(local_ctx); diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index d9799a50e108..2954c046a501 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -149,10 +149,4 @@ void topology_add_sync_waiter_(const tal_t *ctx, (arg)) -/* We want to update db when this txid is confirmed. We always do this - * if it's related to a channel or incoming funds, but sendpsbt without - * change would be otherwise untracked. */ -void watch_unconfirmed_txid(struct lightningd *ld, - struct chain_topology *topo, - const struct bitcoin_txid *txid); #endif /* LIGHTNING_LIGHTNINGD_CHAINTOPOLOGY_H */ diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index d193b6904ac0..5eb1181c4408 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -331,21 +331,6 @@ static bool channel_splice_watch_found(struct lightningd *ld, const struct short_channel_id *scid, u32 blockheight); -/* We coop-closed channel: if another inflight confirms, force close */ -static void closed_inflight_splice_found(struct lightningd *ld, - const struct bitcoin_tx *tx, - u32 outnum, - const struct txlocator *loc, - struct channel_inflight *inflight) -{ - /* This is now the main tx. */ - update_channel_from_inflight(ld, inflight->channel, inflight, false); - channel_fail_saw_onchain(inflight->channel, - REASON_UNKNOWN, - tx, - "Inflight tx confirmed after mutual close"); -} - void drop_to_chain(struct lightningd *ld, struct channel *channel, bool cooperative, const struct bitcoin_tx *unilateral_tx) @@ -468,27 +453,6 @@ void drop_to_chain(struct lightningd *ld, struct channel *channel, resolve_close_command(ld, channel, cooperative, txs); } - /* In cooperative mode, we're assuming that we closed the right one: - * this might not happen if we're splicing, or dual-funding still - * opening. So, if we get any unexpected inflight confirming, we - * force close. */ - if (cooperative) { - list_for_each(&channel->inflights, inflight, list) { - if (bitcoin_outpoint_eq(&inflight->funding->outpoint, - &channel->funding)) { - continue; - } - const u8 *funding_wscript = bitcoin_redeem_2of2(tmpctx, - &channel->local_funding_pubkey, - inflight->funding->splice_remote_funding); - watch_scriptpubkey(inflight, ld->topology, - take(scriptpubkey_p2wsh(NULL, funding_wscript)), - &inflight->funding->outpoint, - inflight->funding->total_funds, - closed_inflight_splice_found, - inflight); - } - } } void resend_closing_transactions(struct lightningd *ld) diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c index b170fa7dde63..92fe45e83ab3 100644 --- a/wallet/walletrpc.c +++ b/wallet/walletrpc.c @@ -1014,12 +1014,9 @@ static void sendpsbt_done(struct bitcoind *bitcoind UNUSED, wallet_transaction_add(ld->wallet, sending->wtx, 0, 0); wally_txid(sending->wtx, &txid); - /* Extract the change output and add it to the DB */ - if (!wallet_extract_owned_outputs(ld->wallet, sending->wtx, false, NULL, NULL)) { - /* If we're not watching it for selfish reasons (i.e. pure send to - * others), make sure we're watching it so we can update depth in db */ - watch_unconfirmed_txid(ld, ld->topology, &txid); - } + /* Extract the change output and add it to the DB; bwatch handles the + * confirmation tracking via wallet UTXO watches. */ + wallet_extract_owned_outputs(ld->wallet, sending->wtx, false, NULL, NULL); for (size_t i = 0; i < sending->psbt->num_outputs; i++) maybe_notify_new_external_send(ld, &txid, i, sending->psbt); From 64cd813cfa8dca798cfefa8de8960867ff46b629 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 11:54:17 +0930 Subject: [PATCH 76/77] lightningd: delete legacy watch.{c,h} --- lightningd/Makefile | 3 +- lightningd/chaintopology.c | 48 +- lightningd/chaintopology.h | 11 +- lightningd/channel.c | 2 - lightningd/channel.h | 3 - lightningd/test/run-invoice-select-inchan.c | 20 - lightningd/watch.c | 620 -------------------- lightningd/watch.h | 189 ------ wallet/test/run-wallet.c | 28 - 9 files changed, 4 insertions(+), 920 deletions(-) delete mode 100644 lightningd/watch.c delete mode 100644 lightningd/watch.h diff --git a/lightningd/Makefile b/lightningd/Makefile index ac272645f19e..77c2d24e1fdb 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -45,8 +45,7 @@ LIGHTNINGD_SRC := \ lightningd/routehint.c \ lightningd/runes.c \ lightningd/subd.c \ - lightningd/wait.c \ - lightningd/watch.c + lightningd/wait.c LIGHTNINGD_SRC_NOHDR := \ lightningd/configs.c \ diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index c970cd01b81b..011285000f8d 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -38,27 +38,9 @@ static void filter_block_txs(struct chain_topology *topo, struct block *b) for (size_t i = 0; i < num_txs; i++) { struct bitcoin_tx *tx = b->full_txs[i]; struct bitcoin_txid txid; - const struct txlocator loc = { b->height, i }; bool is_coinbase = i == 0; size_t *our_outnums; - /* Tell them if it spends a txo we care about. */ - for (size_t j = 0; j < tx->wtx->num_inputs; j++) { - struct bitcoin_outpoint out; - struct txowatch_hash_iter it; - - bitcoin_tx_input_get_txid(tx, j, &out.txid); - out.n = tx->wtx->inputs[j].index; - - for (struct txowatch *txo = txowatch_hash_getfirst(topo->txowatches, &out, &it); - txo; - txo = txowatch_hash_getnext(topo->txowatches, &out, &it)) { - wallet_transaction_add(topo->ld->wallet, - tx->wtx, b->height, i); - txowatch_fire(txo, tx, j, b); - } - } - txid = b->txids[i]; our_outnums = tal_arr(tmpctx, size_t, 0); if (wallet_extract_owned_outputs(topo->ld->wallet, @@ -79,17 +61,11 @@ static void filter_block_txs(struct chain_topology *topo, struct block *b) } - /* We did spends first, in case that tells us to watch tx. */ - /* Make sure we preserve any transaction we are interested in */ - if (watch_check_tx_outputs(topo, &loc, tx, &txid) - || watching_txid(topo, &txid) - || we_broadcast(topo->ld, &txid)) { + if (we_broadcast(topo->ld, &txid)) { wallet_transaction_add(topo->ld->wallet, tx->wtx, b->height, i); } - - txwatch_inform(topo, &txid, take(tx)); } b->full_txs = tal_free(b->full_txs); b->txids = tal_free(b->txids); @@ -137,12 +113,6 @@ static void updates_complete(struct chain_topology *topo) /* Tell lightningd about new block. */ notify_new_block(topo->ld); - /* Tell blockdepth watchers */ - watch_check_block_added(topo, topo->tip->height); - - /* Tell watch code to re-evaluate all txs. */ - watch_topology_changed(topo); - /* Maybe need to rebroadcast. */ rebroadcast_txs(topo->ld); @@ -316,8 +286,6 @@ static struct block *new_block(struct chain_topology *topo, static void remove_tip(struct chain_topology *topo) { struct block *b = topo->tip; - struct bitcoin_txid *txs; - size_t n; const struct short_channel_id *removed_scids; log_debug(topo->log, "Removing stale block %u: %s", @@ -332,21 +300,11 @@ static void remove_tip(struct chain_topology *topo) b->height, fmt_bitcoin_blkid(tmpctx, &b->blkid)); - txs = wallet_transactions_by_height(b, topo->ld->wallet, b->height); - n = tal_count(txs); - - /* Notify that txs are kicked out (their height will be set NULL in db) */ - for (size_t i = 0; i < n; i++) - txwatch_fire(topo, &txs[i], 0); - /* Grab these before we delete block from db */ removed_scids = wallet_utxoset_get_created(tmpctx, topo->ld->wallet, b->height); wallet_block_remove(topo->ld->wallet, b); - /* Anyone watching for block removes */ - watch_check_block_removed(topo, b->height); - block_map_del(topo->block_map, b); /* These no longer exist, so gossipd drops any reference to them just @@ -422,10 +380,6 @@ struct chain_topology *new_topology(struct lightningd *ld, struct logger *log) topo->ld = ld; topo->block_map = new_htable(topo, block_map); - topo->txwatches = new_htable(topo, txwatch_hash); - topo->txowatches = new_htable(topo, txowatch_hash); - topo->scriptpubkeywatches = new_htable(topo, scriptpubkeywatch_hash); - topo->blockdepthwatches = new_htable(topo, blockdepthwatch_hash); topo->log = log; topo->root = NULL; topo->sync_waiters = tal(topo, struct list_head); diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index 2954c046a501..12fc4d8e9150 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -1,17 +1,16 @@ #ifndef LIGHTNING_LIGHTNINGD_CHAINTOPOLOGY_H #define LIGHTNING_LIGHTNINGD_CHAINTOPOLOGY_H #include "config.h" +#include +#include #include #include -#include struct bitcoin_tx; struct bitcoind; struct command; struct lightningd; struct peer; -struct txwatch; -struct scriptpubkeywatch; struct wallet; struct block { @@ -73,12 +72,6 @@ struct chain_topology { /* Timers we're running. */ struct oneshot *checkchain_timer, *extend_timer; - /* Transactions/txos we are watching. */ - struct txwatch_hash *txwatches; - struct txowatch_hash *txowatches; - struct scriptpubkeywatch_hash *scriptpubkeywatches; - struct blockdepthwatch_hash *blockdepthwatches; - /* The number of headers known to the bitcoin backend at startup. Not * updated after the initial check. */ u32 headercount; diff --git a/lightningd/channel.c b/lightningd/channel.c index 3cb07d40aefe..c773f101a335 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -375,7 +375,6 @@ struct channel *new_unsaved_channel(struct peer *peer, channel->next_index[LOCAL] = 1; channel->next_index[REMOTE] = 1; channel->next_htlc_id = 0; - channel->funding_spend_watch = NULL; channel->pre_splice_funding = NULL; channel->onchaind_watches = NULL; channel->funding_spend_txid = NULL; @@ -615,7 +614,6 @@ struct channel *new_channel(struct peer *peer, u64 dbid, channel->next_htlc_id = next_htlc_id; channel->funding = *funding; channel->funding_sats = funding_sats; - channel->funding_spend_watch = NULL; channel->pre_splice_funding = NULL; channel->onchaind_watches = NULL; channel->funding_spend_txid = NULL; diff --git a/lightningd/channel.h b/lightningd/channel.h index 092437c83a29..f8e36df368ca 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -206,9 +206,6 @@ struct channel { struct bitcoin_outpoint funding; struct amount_sat funding_sats; - /* Watch we have on funding output. */ - struct txowatch *funding_spend_watch; - /* Original funding outpoint before a splice overwrites channel->funding. * Populated by channel_splice_watch_found; read by handle_peer_splice_locked * for channel_record_splice. In-memory only: not persisted to the wallet. diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index 7a615ee0f819..cdb055994756 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -79,13 +79,6 @@ void channel_fail_permanent(struct channel *channel UNNEEDED, const char *fmt UNNEEDED, ...) { fprintf(stderr, "channel_fail_permanent called!\n"); abort(); } -/* Generated stub for channel_fail_saw_onchain */ -void channel_fail_saw_onchain(struct channel *channel UNNEEDED, - enum state_change reason UNNEEDED, - const struct bitcoin_tx *tx UNNEEDED, - const char *fmt UNNEEDED, - ...) -{ fprintf(stderr, "channel_fail_saw_onchain called!\n"); abort(); } /* Generated stub for channel_fail_transient */ void channel_fail_transient(struct channel *channel UNNEEDED, bool disconnect UNNEEDED, @@ -768,19 +761,6 @@ void wallet_unreserve_utxo(struct wallet *w UNNEEDED, struct utxo *utxo UNNEEDED struct utxo *wallet_utxo_get(const tal_t *ctx UNNEEDED, struct wallet *w UNNEEDED, const struct bitcoin_outpoint *outpoint UNNEEDED) { fprintf(stderr, "wallet_utxo_get called!\n"); abort(); } -/* Generated stub for watch_scriptpubkey_ */ -bool watch_scriptpubkey_(const tal_t *ctx UNNEEDED, - struct chain_topology *topo UNNEEDED, - const u8 *scriptpubkey TAKES UNNEEDED, - const struct bitcoin_outpoint *expected_outpoint UNNEEDED, - struct amount_sat expected_amount UNNEEDED, - void (*cb)(struct lightningd *ld UNNEEDED, - const struct bitcoin_tx *tx UNNEEDED, - u32 outnum UNNEEDED, - const struct txlocator *loc UNNEEDED, - void *) UNNEEDED, - void *arg UNNEEDED) -{ fprintf(stderr, "watch_scriptpubkey_ called!\n"); abort(); } /* Generated stub for watchman_unwatch_blockdepth */ void watchman_unwatch_blockdepth(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, diff --git a/lightningd/watch.c b/lightningd/watch.c deleted file mode 100644 index f22cf6983328..000000000000 --- a/lightningd/watch.c +++ /dev/null @@ -1,620 +0,0 @@ -/* Code to talk to bitcoind to watch for various events. - * - * Here's what we want to know: - * - * - An anchor tx: - * - Reached given depth - * - Times out. - * - Is unspent after reaching given depth. - * - * - Our own commitment tx: - * - Reached a given depth. - * - * - HTLC spend tx: - * - Reached a given depth. - * - * - Anchor tx output: - * - Is spent by their current tx. - * - Is spent by a revoked tx. - * - * - Commitment tx HTLC outputs: - * - HTLC timed out - * - HTLC spent - * - * - Payments to invoice fallback addresses: - * - Reached a given depth. - * - * We do this by adding the P2SH address to the wallet, and then querying - * that using listtransactions. - * - * WE ASSUME NO MALLEABILITY! This requires segregated witness. - */ -#include "config.h" -#include -#include -#include -#include -#include -#include -#include - -/* Watching an output */ -struct txowatch { - struct chain_topology *topo; - - /* Channel who owns us. */ - struct channel *channel; - - /* Output to watch. */ - struct bitcoin_outpoint out; - - /* A new tx. */ - enum watch_result (*cb)(struct channel *channel, - const struct bitcoin_tx *tx, - size_t input_num, - const struct block *block); -}; - -struct txwatch { - struct chain_topology *topo; - - /* Transaction to watch. */ - struct bitcoin_txid txid; - - /* May be NULL if we haven't seen it yet. */ - const struct bitcoin_tx *tx; - - int depth; - - /* A new depth (0 if kicked out, otherwise 1 = tip, etc.) */ - enum watch_result (*cb)(struct lightningd *ld, - const struct bitcoin_txid *txid, - const struct bitcoin_tx *tx, - unsigned int depth, - void *arg); - void *cbarg; -}; - -const struct bitcoin_outpoint *txowatch_keyof(const struct txowatch *w) -{ - return &w->out; -} - -size_t txo_hash(const struct bitcoin_outpoint *out) -{ - /* This hash-in-one-go trick only works if they're consecutive. */ - BUILD_ASSERT(offsetof(struct bitcoin_outpoint, n) - == sizeof(((struct bitcoin_outpoint *)NULL)->txid)); - return siphash24(siphash_seed(), out, - sizeof(out->txid) + sizeof(out->n)); -} - -bool txowatch_eq(const struct txowatch *w, const struct bitcoin_outpoint *out) -{ - return bitcoin_txid_eq(&w->out.txid, &out->txid) - && w->out.n == out->n; -} - -static void destroy_txowatch(struct txowatch *w) -{ - txowatch_hash_del(w->topo->txowatches, w); -} - -const struct bitcoin_txid *txwatch_keyof(const struct txwatch *w) -{ - return &w->txid; -} - -size_t txid_hash(const struct bitcoin_txid *txid) -{ - return siphash24(siphash_seed(), - txid->shad.sha.u.u8, sizeof(txid->shad.sha.u.u8)); -} - -bool txwatch_eq(const struct txwatch *w, const struct bitcoin_txid *txid) -{ - return bitcoin_txid_eq(&w->txid, txid); -} - -static void destroy_txwatch(struct txwatch *w) -{ - txwatch_hash_del(w->topo->txwatches, w); -} - -struct txwatch *watch_txid_(const tal_t *ctx, - struct chain_topology *topo, - const struct bitcoin_txid *txid, - enum watch_result (*cb)(struct lightningd *ld, - const struct bitcoin_txid *, - const struct bitcoin_tx *, - unsigned int depth, - void *arg), - void *arg) -{ - struct txwatch *w; - - w = tal(ctx, struct txwatch); - w->topo = topo; - w->depth = -1; - w->txid = *txid; - w->tx = NULL; - w->cb = cb; - w->cbarg = arg; - - txwatch_hash_add(w->topo->txwatches, w); - tal_add_destructor(w, destroy_txwatch); - - return w; -} - -struct txwatch *find_txwatch_(struct chain_topology *topo, - const struct bitcoin_txid *txid, - enum watch_result (*cb)(struct lightningd *ld, - const struct bitcoin_txid *, - const struct bitcoin_tx *, - unsigned int depth, - void *arg), - void *arg) -{ - struct txwatch_hash_iter i; - struct txwatch *w; - - /* We could have more than one channel watching same txid, though we - * don't for onchaind. */ - for (w = txwatch_hash_getfirst(topo->txwatches, txid, &i); - w; - w = txwatch_hash_getnext(topo->txwatches, txid, &i)) { - if (w->cb == cb && w->cbarg == arg) - break; - } - return w; -} - -bool watching_txid(const struct chain_topology *topo, - const struct bitcoin_txid *txid) -{ - return txwatch_hash_exists(topo->txwatches, txid); -} - -struct txowatch *watch_txo(const tal_t *ctx, - struct chain_topology *topo, - struct channel *channel, - const struct bitcoin_outpoint *outpoint, - enum watch_result (*cb)(struct channel *channel_, - const struct bitcoin_tx *tx, - size_t input_num, - const struct block *block)) -{ - struct txowatch *w = tal(ctx, struct txowatch); - - w->topo = topo; - w->out = *outpoint; - w->channel = channel; - w->cb = cb; - - txowatch_hash_add(w->topo->txowatches, w); - tal_add_destructor(w, destroy_txowatch); - - return w; -} - -/* Returns true if we fired a callback */ -static bool txw_fire(struct txwatch *txw, - const struct bitcoin_txid *txid, - unsigned int depth) -{ - enum watch_result r; - - if (depth == txw->depth) - return false; - - if (txw->depth == -1) { - log_debug(txw->topo->log, - "Got first depth change 0->%u for %s", - depth, - fmt_bitcoin_txid(tmpctx, &txw->txid)); - } else { - /* zero depth signals a reorganization */ - log_debug(txw->topo->log, - "Got depth change %u->%u for %s%s", - txw->depth, depth, - fmt_bitcoin_txid(tmpctx, &txw->txid), - depth ? "" : " REORG"); - } - txw->depth = depth; - r = txw->cb(txw->topo->ld, txid, txw->tx, txw->depth, - txw->cbarg); - switch (r) { - case DELETE_WATCH: - tal_free(txw); - return true; - case KEEP_WATCHING: - return true; - } - fatal("txwatch callback %p returned %i\n", txw->cb, r); -} - -void txwatch_fire(struct chain_topology *topo, - const struct bitcoin_txid *txid, - unsigned int depth) -{ - struct txwatch_hash_iter it; - - for (struct txwatch *txw = txwatch_hash_getfirst(topo->txwatches, txid, &it); - txw; - txw = txwatch_hash_getnext(topo->txwatches, txid, &it)) { - txw_fire(txw, txid, depth); - } -} - -void txowatch_fire(const struct txowatch *txow, - const struct bitcoin_tx *tx, - size_t input_num, - const struct block *block) -{ - struct bitcoin_txid txid; - enum watch_result r; - - bitcoin_txid(tx, &txid); - log_debug(txow->channel->log, - "Got UTXO spend for %s:%u: %s", - fmt_bitcoin_txid(tmpctx, &txow->out.txid), - txow->out.n, - fmt_bitcoin_txid(tmpctx, &txid)); - - r = txow->cb(txow->channel, tx, input_num, block); - switch (r) { - case DELETE_WATCH: - tal_free(txow); - return; - case KEEP_WATCHING: - return; - } - fatal("txowatch callback %p returned %i", txow->cb, r); -} - -void watch_topology_changed(struct chain_topology *topo) -{ - struct txwatch_hash_iter i; - struct txwatch *w; - - /* Iterating a htable during deletes is safe and consistent. - * Adding is forbidden. */ - txwatch_hash_lock(topo->txwatches); - for (w = txwatch_hash_first(topo->txwatches, &i); - w; - w = txwatch_hash_next(topo->txwatches, &i)) { - u32 depth; - - depth = get_tx_depth(topo, &w->txid); - if (depth) { - if (!w->tx) - w->tx = wallet_transaction_get(w, topo->ld->wallet, - &w->txid); - txw_fire(w, &w->txid, depth); - } - } - txwatch_hash_unlock(topo->txwatches); -} - -void txwatch_inform(const struct chain_topology *topo, - const struct bitcoin_txid *txid, - struct bitcoin_tx *tx TAKES) -{ - struct txwatch_hash_iter it; - - for (struct txwatch *txw = txwatch_hash_getfirst(topo->txwatches, txid, &it); - txw; - txw = txwatch_hash_getnext(topo->txwatches, txid, &it)) { - if (txw->tx) - continue; - /* FIXME: YUCK! These don't have PSBTs attached */ - if (!tx->psbt) - tx->psbt = new_psbt(tx, tx->wtx); - txw->tx = clone_bitcoin_tx(txw, tx); - } - - /* If we don't clone above, handle take() now */ - tal_free_if_taken(tx); -} - -struct scriptpubkeywatch { - struct script_with_len swl; - struct bitcoin_outpoint expected_outpoint; - struct amount_sat expected_amount; - void (*cb)(struct lightningd *ld, - const struct bitcoin_tx *tx, - u32 outnum, - const struct txlocator *loc, - void *); - void *arg; -}; - -const struct script_with_len *scriptpubkeywatch_keyof(const struct scriptpubkeywatch *w) -{ - return &w->swl; -} - -bool scriptpubkeywatch_eq(const struct scriptpubkeywatch *w, const struct script_with_len *swl) -{ - return script_with_len_eq(&w->swl, swl); -} - -static void destroy_scriptpubkeywatch(struct scriptpubkeywatch *w, struct chain_topology *topo) -{ - scriptpubkeywatch_hash_del(topo->scriptpubkeywatches, w); -} - -static struct scriptpubkeywatch *find_watchscriptpubkey(const struct scriptpubkeywatch_hash *scriptpubkeywatches, - const u8 *scriptpubkey, - const struct bitcoin_outpoint *expected_outpoint, - struct amount_sat expected_amount, - void (*cb)(struct lightningd *ld, - const struct bitcoin_tx *tx, - u32 outnum, - const struct txlocator *loc, - void *), - void *arg) -{ - struct scriptpubkeywatch_hash_iter it; - const struct script_with_len swl = { scriptpubkey, tal_bytelen(scriptpubkey) }; - - for (struct scriptpubkeywatch *w = scriptpubkeywatch_hash_getfirst(scriptpubkeywatches, &swl, &it); - w; - w = scriptpubkeywatch_hash_getnext(scriptpubkeywatches, &swl, &it)) { - if (bitcoin_outpoint_eq(&w->expected_outpoint, expected_outpoint) - && amount_sat_eq(w->expected_amount, expected_amount) - && w->cb == cb - && w->arg == arg) { - return w; - } - } - return NULL; -} - -bool watch_scriptpubkey_(const tal_t *ctx, - struct chain_topology *topo, - const u8 *scriptpubkey TAKES, - const struct bitcoin_outpoint *expected_outpoint, - struct amount_sat expected_amount, - void (*cb)(struct lightningd *ld, - const struct bitcoin_tx *tx, - u32 outnum, - const struct txlocator *loc, - void *), - void *arg) -{ - struct scriptpubkeywatch *w; - - if (find_watchscriptpubkey(topo->scriptpubkeywatches, - scriptpubkey, - expected_outpoint, - expected_amount, - cb, arg)) { - if (taken(scriptpubkey)) - tal_free(scriptpubkey); - return false; - } - - w = tal(ctx, struct scriptpubkeywatch); - w->swl.script = tal_dup_talarr(w, u8, scriptpubkey); - w->swl.len = tal_bytelen(w->swl.script); - w->expected_outpoint = *expected_outpoint; - w->expected_amount = expected_amount; - w->cb = cb; - w->arg = arg; - scriptpubkeywatch_hash_add(topo->scriptpubkeywatches, w); - tal_add_destructor2(w, destroy_scriptpubkeywatch, topo); - return true; -} - -bool unwatch_scriptpubkey_(const tal_t *ctx, - struct chain_topology *topo, - const u8 *scriptpubkey, - const struct bitcoin_outpoint *expected_outpoint, - struct amount_sat expected_amount, - void (*cb)(struct lightningd *ld, - const struct bitcoin_tx *tx, - u32 outnum, - const struct txlocator *loc, - void *), - void *arg) -{ - struct scriptpubkeywatch *w = find_watchscriptpubkey(topo->scriptpubkeywatches, - scriptpubkey, - expected_outpoint, - expected_amount, - cb, arg); - if (w) { - tal_free(w); - return true; - } - return false; -} - -bool watch_check_tx_outputs(const struct chain_topology *topo, - const struct txlocator *loc, - const struct bitcoin_tx *tx, - const struct bitcoin_txid *txid) -{ - bool tx_interesting = false; - - for (size_t outnum = 0; outnum < tx->wtx->num_outputs; outnum++) { - const struct wally_tx_output *txout = &tx->wtx->outputs[outnum]; - const struct script_with_len swl = { txout->script, txout->script_len }; - struct scriptpubkeywatch_hash_iter it; - bool output_matched = false, bad_txid = false, bad_amount = false, bad_outnum = false; - struct amount_asset outasset = bitcoin_tx_output_get_amount(tx, outnum); - - /* Ensure callbacks don't do an insert during iteration! */ - scriptpubkeywatch_hash_lock(topo->scriptpubkeywatches); - for (struct scriptpubkeywatch *w = scriptpubkeywatch_hash_getfirst(topo->scriptpubkeywatches, &swl, &it); - w; - w = scriptpubkeywatch_hash_getnext(topo->scriptpubkeywatches, &swl, &it)) { - if (!bitcoin_txid_eq(&w->expected_outpoint.txid, txid)) { - bad_txid = true; - continue; - } - if (outnum != w->expected_outpoint.n) { - bad_outnum = true; - continue; - } - if (!amount_asset_is_main(&outasset) - || !amount_sat_eq(amount_asset_to_sat(&outasset), w->expected_amount)) { - bad_amount = true; - continue; - } - - w->cb(topo->ld, tx, outnum, loc, w->arg); - output_matched = true; - tx_interesting = true; - } - scriptpubkeywatch_hash_unlock(topo->scriptpubkeywatches); - - /* Only complain about mismatch if we missed all of them. - * This helps diagnose mistakes like wrong txid, see - * https://github.com/ElementsProject/lightning/issues/8892 */ - if (!output_matched && (bad_txid || bad_amount || bad_outnum)) { - const char *addr = encode_scriptpubkey_to_addr(tmpctx, chainparams, - txout->script, txout->script_len); - if (!addr) - addr = tal_fmt(tmpctx, "Scriptpubkey %s", tal_hexstr(tmpctx, txout->script, txout->script_len)); - if (bad_txid) { - log_unusual(topo->ld->log, - "Unexpected spend to %s by unexpected txid %s:%zu", - addr, fmt_bitcoin_txid(tmpctx, txid), outnum); - } - if (bad_amount) { - log_unusual(topo->ld->log, - "Unexpected amount %s to %s by txid %s:%zu", - amount_asset_is_main(&outasset) - ? fmt_amount_sat(tmpctx, amount_asset_to_sat(&outasset)) - : "fee output", - addr, fmt_bitcoin_txid(tmpctx, txid), outnum); - } - if (bad_outnum) { - log_unusual(topo->ld->log, - "Unexpected output number %zu paying to %s in txid %s", - outnum, addr, fmt_bitcoin_txid(tmpctx, txid)); - } - } - } - - return tx_interesting; -} - -struct blockdepthwatch { - u32 height; - enum watch_result (*depthcb)(struct lightningd *ld, - u32 depth, - void *); - enum watch_result (*reorgcb)(struct lightningd *ld, - void *); - void *arg; -}; - -u32 blockdepthwatch_keyof(const struct blockdepthwatch *w) -{ - return w->height; -} - -size_t u32_hash(u32 val) -{ - return siphash24(siphash_seed(), &val, sizeof(val)); -} - -bool blockdepthwatch_eq(const struct blockdepthwatch *w, u32 height) -{ - return w->height == height; -} - -static void destroy_blockdepthwatch(struct blockdepthwatch *w, struct chain_topology *topo) -{ - blockdepthwatch_hash_del(topo->blockdepthwatches, w); -} - -static struct blockdepthwatch *find_blockdepthwatch(const struct blockdepthwatch_hash *blockdepthwatches, - u32 blockheight, - enum watch_result (*depthcb)(struct lightningd *ld, u32 depth, void *), - enum watch_result (*reorgcb)(struct lightningd *ld, void *), - void *arg) -{ - struct blockdepthwatch_hash_iter it; - - for (struct blockdepthwatch *w = blockdepthwatch_hash_first(blockdepthwatches, &it); - w; - w = blockdepthwatch_hash_next(blockdepthwatches, &it)) { - if (w->height == blockheight - && w->depthcb == depthcb - && w->reorgcb == reorgcb - && w->arg == arg) { - return w; - } - } - return NULL; -} - -bool watch_blockdepth_(const tal_t *ctx, - struct chain_topology *topo, - u32 blockheight, - enum watch_result (*depthcb)(struct lightningd *ld, u32 depth, void *), - enum watch_result (*reorgcb)(struct lightningd *ld, void *), - void *arg) -{ - struct blockdepthwatch *w; - - if (find_blockdepthwatch(topo->blockdepthwatches, blockheight, depthcb, reorgcb, arg)) - return false; - - w = tal(ctx, struct blockdepthwatch); - w->height = blockheight; - w->depthcb = depthcb; - w->reorgcb = reorgcb; - w->arg = arg; - blockdepthwatch_hash_add(topo->blockdepthwatches, w); - tal_add_destructor2(w, destroy_blockdepthwatch, topo); - return true; -} - -void watch_check_block_added(const struct chain_topology *topo, u32 blockheight) -{ - struct blockdepthwatch_hash_iter it; - - /* With ccan/htable, deleting during iteration is safe: adding isn't! */ - blockdepthwatch_hash_lock(topo->blockdepthwatches); - for (struct blockdepthwatch *w = blockdepthwatch_hash_first(topo->blockdepthwatches, &it); - w; - w = blockdepthwatch_hash_next(topo->blockdepthwatches, &it)) { - /* You are not supposed to watch future blocks! */ - assert(blockheight >= w->height); - - u32 depth = blockheight - w->height + 1; - enum watch_result r = w->depthcb(topo->ld, depth, w->arg); - - switch (r) { - case DELETE_WATCH: - tal_free(w); - continue; - case KEEP_WATCHING: - continue; - } - fatal("blockdepthwatch depth callback %p returned %i", w->depthcb, r); - } - blockdepthwatch_hash_unlock(topo->blockdepthwatches); -} - -void watch_check_block_removed(const struct chain_topology *topo, u32 blockheight) -{ - struct blockdepthwatch_hash_iter it; - - /* With ccan/htable, deleting during iteration is safe. */ - blockdepthwatch_hash_lock(topo->blockdepthwatches); - for (struct blockdepthwatch *w = blockdepthwatch_hash_getfirst(topo->blockdepthwatches, blockheight, &it); - w; - w = blockdepthwatch_hash_getnext(topo->blockdepthwatches, blockheight, &it)) { - enum watch_result r = w->reorgcb(topo->ld, w->arg); - assert(r == DELETE_WATCH); - tal_free(w); - } - blockdepthwatch_hash_unlock(topo->blockdepthwatches); -} diff --git a/lightningd/watch.h b/lightningd/watch.h deleted file mode 100644 index ec209a6d7317..000000000000 --- a/lightningd/watch.h +++ /dev/null @@ -1,189 +0,0 @@ -#ifndef LIGHTNING_LIGHTNINGD_WATCH_H -#define LIGHTNING_LIGHTNINGD_WATCH_H -#include "config.h" -#include -#include -#include - -struct block; -struct channel; -struct chain_topology; -struct lightningd; -struct txlocator; -struct txowatch; -struct txwatch; -struct scriptpubkeywatch; -struct blockdepthwatch; - -enum watch_result { - DELETE_WATCH = -1, - KEEP_WATCHING = -2 -}; - -const struct bitcoin_outpoint *txowatch_keyof(const struct txowatch *w); -size_t txo_hash(const struct bitcoin_outpoint *out); -bool txowatch_eq(const struct txowatch *w, const struct bitcoin_outpoint *out); - -HTABLE_DEFINE_DUPS_TYPE(struct txowatch, txowatch_keyof, txo_hash, txowatch_eq, - txowatch_hash); - -const struct bitcoin_txid *txwatch_keyof(const struct txwatch *w); -size_t txid_hash(const struct bitcoin_txid *txid); -bool txwatch_eq(const struct txwatch *w, const struct bitcoin_txid *txid); -HTABLE_DEFINE_DUPS_TYPE(struct txwatch, txwatch_keyof, txid_hash, txwatch_eq, - txwatch_hash); - -const struct script_with_len *scriptpubkeywatch_keyof(const struct scriptpubkeywatch *w); -bool scriptpubkeywatch_eq(const struct scriptpubkeywatch *w, const struct script_with_len *swl); -HTABLE_DEFINE_DUPS_TYPE(struct scriptpubkeywatch, scriptpubkeywatch_keyof, script_with_len_hash, scriptpubkeywatch_eq, - scriptpubkeywatch_hash); - -u32 blockdepthwatch_keyof(const struct blockdepthwatch *w); -size_t u32_hash(u32); -bool blockdepthwatch_eq(const struct blockdepthwatch *w, u32 height); -HTABLE_DEFINE_DUPS_TYPE(struct blockdepthwatch, blockdepthwatch_keyof, u32_hash, blockdepthwatch_eq, - blockdepthwatch_hash); - -struct txwatch *watch_txid_(const tal_t *ctx, - struct chain_topology *topo, - const struct bitcoin_txid *txid, - enum watch_result (*cb)(struct lightningd *ld, - const struct bitcoin_txid *, - const struct bitcoin_tx *, - unsigned int depth, - void *arg), - void *arg); - -#define watch_txid(ctx, topo, txid, cb, arg) \ - watch_txid_((ctx), (topo), (txid), \ - typesafe_cb_preargs(enum watch_result, void *, \ - (cb), (arg), \ - struct lightningd *, \ - const struct bitcoin_txid *, \ - const struct bitcoin_tx *, \ - unsigned int depth), \ - (arg)) - -struct txowatch *watch_txo(const tal_t *ctx, - struct chain_topology *topo, - struct channel *channel, - const struct bitcoin_outpoint *outpoint, - enum watch_result (*cb)(struct channel *, - const struct bitcoin_tx *tx, - size_t input_num, - const struct block *block)); - -struct txwatch *find_txwatch_(struct chain_topology *topo, - const struct bitcoin_txid *txid, - enum watch_result (*cb)(struct lightningd *ld, - const struct bitcoin_txid *, - const struct bitcoin_tx *, - unsigned int depth, - void *arg), - void *arg); - -#define find_txwatch(topo, txid, cb, arg) \ - find_txwatch_((topo), (txid), \ - typesafe_cb_preargs(enum watch_result, void *, \ - (cb), (arg), \ - struct lightningd *, \ - const struct bitcoin_txid *, \ - const struct bitcoin_tx *, \ - unsigned int depth), \ - (arg)) - -void txwatch_fire(struct chain_topology *topo, - const struct bitcoin_txid *txid, - unsigned int depth); - -void txowatch_fire(const struct txowatch *txow, - const struct bitcoin_tx *tx, size_t input_num, - const struct block *block); - -bool watching_txid(const struct chain_topology *topo, - const struct bitcoin_txid *txid); - -void txwatch_inform(const struct chain_topology *topo, - const struct bitcoin_txid *txid, - struct bitcoin_tx *tx TAKES); - -/* Watch for specific spends to this scriptpubkey: returns false if was already watched. */ -bool watch_scriptpubkey_(const tal_t *ctx, - struct chain_topology *topo, - const u8 *scriptpubkey TAKES, - const struct bitcoin_outpoint *expected_outpoint, - struct amount_sat expected_amount, - void (*cb)(struct lightningd *ld, - const struct bitcoin_tx *tx, - u32 outnum, - const struct txlocator *loc, - void *), - void *arg); - -#define watch_scriptpubkey(ctx, topo, scriptpubkey, expected_outpoint, expected_amount, cb, arg) \ - watch_scriptpubkey_((ctx), (topo), (scriptpubkey), \ - (expected_outpoint), (expected_amount), \ - typesafe_cb_preargs(void, void *, \ - (cb), (arg), \ - struct lightningd *, \ - const struct bitcoin_tx *, \ - u32 outnum, \ - const struct txlocator *), \ - (arg)) - -bool unwatch_scriptpubkey_(const tal_t *ctx, - struct chain_topology *topo, - const u8 *scriptpubkey, - const struct bitcoin_outpoint *expected_outpoint, - struct amount_sat expected_amount, - void (*cb)(struct lightningd *ld, - const struct bitcoin_tx *tx, - u32 outnum, - const struct txlocator *loc, - void *), - void *arg); - -#define unwatch_scriptpubkey(ctx, topo, scriptpubkey, expected_outpoint, expected_amount, cb, arg) \ - unwatch_scriptpubkey_((ctx), (topo), (scriptpubkey), \ - (expected_outpoint), (expected_amount), \ - typesafe_cb_preargs(void, void *, \ - (cb), (arg), \ - struct lightningd *, \ - const struct bitcoin_tx *, \ - u32 outnum, \ - const struct txlocator *), \ - (arg)) - -/* Watch for this block getting deeper (or reorged out). Returns false it if was a duplicate. */ -bool watch_blockdepth_(const tal_t *ctx, - struct chain_topology *topo, - u32 blockheight, - enum watch_result (*depthcb)(struct lightningd *ld, u32 depth, void *), - enum watch_result (*reorgcb)(struct lightningd *ld, void *), - void *arg); - -#define watch_blockdepth(ctx, topo, blockheight, depthcb, reorgcb, arg) \ - watch_blockdepth_((ctx), (topo), (blockheight), \ - typesafe_cb_preargs(enum watch_result, void *, \ - (depthcb), (arg), \ - struct lightningd *, \ - u32), \ - typesafe_cb_preargs(enum watch_result, void *, \ - (reorgcb), (arg), \ - struct lightningd *), \ - (arg)) - -/* Call any scriptpubkey callbacks for this tx */ -bool watch_check_tx_outputs(const struct chain_topology *topo, - const struct txlocator *loc, - const struct bitcoin_tx *tx, - const struct bitcoin_txid *txid); - -/* Call anyone watching for block height increases. */ -void watch_check_block_added(const struct chain_topology *topo, u32 blockheight); - -/* Call anyone watching for block removals. */ -void watch_check_block_removed(const struct chain_topology *topo, u32 blockheight); - -void watch_topology_changed(struct chain_topology *topo); -#endif /* LIGHTNING_LIGHTNINGD_WATCH_H */ diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 8574da97b3ba..3b5acf4d0db7 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -779,19 +779,6 @@ u8 *unsigned_node_announcement(const tal_t *ctx UNNEEDED, struct lightningd *ld UNNEEDED, const u8 *prev UNNEEDED) { fprintf(stderr, "unsigned_node_announcement called!\n"); abort(); } -/* Generated stub for watch_scriptpubkey_ */ -bool watch_scriptpubkey_(const tal_t *ctx UNNEEDED, - struct chain_topology *topo UNNEEDED, - const u8 *scriptpubkey TAKES UNNEEDED, - const struct bitcoin_outpoint *expected_outpoint UNNEEDED, - struct amount_sat expected_amount UNNEEDED, - void (*cb)(struct lightningd *ld UNNEEDED, - const struct bitcoin_tx *tx UNNEEDED, - u32 outnum UNNEEDED, - const struct txlocator *loc UNNEEDED, - void *) UNNEEDED, - void *arg UNNEEDED) -{ fprintf(stderr, "watch_scriptpubkey_ called!\n"); abort(); } /* Generated stub for watchman_unwatch_blockdepth */ void watchman_unwatch_blockdepth(struct lightningd *ld UNNEEDED, const char *owner UNNEEDED, @@ -896,21 +883,6 @@ void migrate_from_account_db(struct lightningd *ld UNNEEDED, struct db *db UNNEE { } -bool unwatch_scriptpubkey_(const tal_t *ctx UNNEEDED, - struct chain_topology *topo UNNEEDED, - const u8 *scriptpubkey TAKES UNNEEDED, - const struct bitcoin_outpoint *expected_outpoint UNNEEDED, - struct amount_sat expected_amount UNNEEDED, - void (*cb)(struct lightningd *ld UNNEEDED, - const struct bitcoin_tx *tx UNNEEDED, - u32 outnum UNNEEDED, - const struct txlocator *loc UNNEEDED, - void *) UNNEEDED, - void *arg UNNEEDED) -{ - return true; -} - /** * mempat -- Set the memory to a pattern * From 11a4d26a78d806dd6ed9d1588befcb7e3f1deb2e Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri Date: Thu, 23 Apr 2026 12:18:44 +0930 Subject: [PATCH 77/77] lightningd: drop chaintopology block tracking, route block height through watchman --- lightningd/bitcoind.h | 1 - lightningd/chaintopology.c | 433 ++--------------------------------- lightningd/chaintopology.h | 54 +---- lightningd/lightningd.c | 14 +- lightningd/notification.c | 13 +- lightningd/notification.h | 4 +- lightningd/onchain_control.h | 1 - lightningd/peer_htlcs.c | 2 +- lightningd/watchman.c | 14 +- wallet/reservation.c | 4 +- wallet/test/run-wallet.c | 10 +- wallet/wallet.c | 77 +------ wallet/wallet.h | 33 +-- 13 files changed, 70 insertions(+), 590 deletions(-) diff --git a/lightningd/bitcoind.h b/lightningd/bitcoind.h index 462817c9cc72..16747586a99b 100644 --- a/lightningd/bitcoind.h +++ b/lightningd/bitcoind.h @@ -7,7 +7,6 @@ struct bitcoin_blkid; struct bitcoin_tx_output; -struct block; struct feerate_est; struct lightningd; struct ripemd160; diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index 011285000f8d..43e44963a208 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -1,75 +1,14 @@ #include "config.h" -#include -#include #include #include -#include -#include #include -#include -#include +#include #include -#include -#include -#include -#include #include -#include -#include -#include - -/* Mutual recursion via timer. */ -static void try_extend_tip(struct chain_topology *topo); - -static bool first_update_complete = false; - -static void next_topology_timer(struct chain_topology *topo) -{ - assert(!topo->extend_timer); - topo->extend_timer = new_reltimer(topo->ld->timers, topo, - time_from_sec(BITCOIND_POLL_SECONDS), - try_extend_tip, topo); -} - -static void filter_block_txs(struct chain_topology *topo, struct block *b) -{ - /* Now we see if any of those txs are interesting. */ - const size_t num_txs = tal_count(b->full_txs); - for (size_t i = 0; i < num_txs; i++) { - struct bitcoin_tx *tx = b->full_txs[i]; - struct bitcoin_txid txid; - bool is_coinbase = i == 0; - size_t *our_outnums; - - txid = b->txids[i]; - our_outnums = tal_arr(tmpctx, size_t, 0); - if (wallet_extract_owned_outputs(topo->ld->wallet, - tx->wtx, is_coinbase, &b->height, &our_outnums)) { - wallet_transaction_add(topo->ld->wallet, tx->wtx, - b->height, i); - for (size_t k = 0; k < tal_count(our_outnums); k++) { - const struct wally_tx_output *txout; - struct amount_sat amount; - struct bitcoin_outpoint outpoint; - - txout = &tx->wtx->outputs[our_outnums[k]]; - outpoint.txid = txid; - outpoint.n = our_outnums[k]; - amount = bitcoin_tx_output_get_amount_sat(tx, our_outnums[k]); - invoice_check_onchain_payment(topo->ld, txout->script, amount, &outpoint); - } - - } - - /* Make sure we preserve any transaction we are interested in */ - if (we_broadcast(topo->ld, &txid)) { - wallet_transaction_add(topo->ld->wallet, - tx->wtx, b->height, i); - } - } - b->full_txs = tal_free(b->full_txs); - b->txids = tal_free(b->txids); -} +#include +#include +#include +#include size_t get_tx_depth(const struct chain_topology *topo, const struct bitcoin_txid *txid) @@ -78,7 +17,7 @@ size_t get_tx_depth(const struct chain_topology *topo, if (blockheight == 0) return 0; - return topo->tip->height - blockheight + 1; + return get_block_height(topo) - blockheight + 1; } struct sync_waiter { @@ -106,263 +45,20 @@ void topology_add_sync_waiter_(const tal_t *ctx, tal_add_destructor(w, destroy_sync_waiter); } -/* Once we're run out of new blocks to add, call this. */ -static void updates_complete(struct chain_topology *topo) -{ - if (!bitcoin_blkid_eq(&topo->tip->blkid, &topo->prev_tip)) { - /* Tell lightningd about new block. */ - notify_new_block(topo->ld); - - /* Maybe need to rebroadcast. */ - rebroadcast_txs(topo->ld); - - /* We've processed these UTXOs */ - db_set_intvar(topo->ld->wallet->db, - "last_processed_block", topo->tip->height); - - topo->prev_tip = topo->tip->blkid; - - /* Send out an account balance snapshot */ - if (!first_update_complete) { - send_account_balance_snapshot(topo->ld); - first_update_complete = true; - } - } - - /* If bitcoind is synced, we're now synced. */ - if (topo->ld->bitcoind->synced && !topology_synced(topo)) { - struct sync_waiter *w; - struct list_head *list = topo->sync_waiters; - - /* Mark topology_synced() before callbacks. */ - topo->sync_waiters = NULL; - - while ((w = list_pop(list, struct sync_waiter, list))) { - /* In case it doesn't free itself. */ - tal_del_destructor(w, destroy_sync_waiter); - tal_steal(list, w); - w->cb(topo, w->arg); - } - tal_free(list); - } - - /* Try again soon. */ - next_topology_timer(topo); -} - -static void record_wallet_spend(struct lightningd *ld, - const struct bitcoin_outpoint *outpoint, - const struct bitcoin_txid *txid, - u32 tx_blockheight) -{ - struct utxo *utxo; - - /* Find the amount this was for */ - utxo = wallet_utxo_get(tmpctx, ld->wallet, outpoint); - if (!utxo) { - log_broken(ld->log, "No record of utxo %s", - fmt_bitcoin_outpoint(tmpctx, - outpoint)); - return; - } - - wallet_save_chain_mvt(ld, new_coin_wallet_withdraw(tmpctx, txid, outpoint, - tx_blockheight, - utxo->amount, mk_mvt_tags(MVT_WITHDRAWAL))); -} - -/** - * topo_update_spends -- Tell the wallet about all spent outpoints - */ -static void topo_update_spends(struct chain_topology *topo, - struct bitcoin_tx **txs, - const struct bitcoin_txid *txids, - u32 blockheight) -{ - const struct short_channel_id *spent_scids; - const size_t num_txs = tal_count(txs); - for (size_t i = 0; i < num_txs; i++) { - const struct bitcoin_tx *tx = txs[i]; - - for (size_t j = 0; j < tx->wtx->num_inputs; j++) { - struct bitcoin_outpoint outpoint; - - bitcoin_tx_input_get_outpoint(tx, j, &outpoint); - - if (wallet_outpoint_spend(tmpctx, topo->ld->wallet, - blockheight, &outpoint)) - record_wallet_spend(topo->ld, &outpoint, - &txids[i], blockheight); - - } - } - - /* Retrieve all potential channel closes from the UTXO set and - * tell gossipd about them. */ - spent_scids = - wallet_utxoset_get_spent(tmpctx, topo->ld->wallet, blockheight); - gossipd_notify_spends(topo->ld, blockheight, spent_scids); -} - -static void topo_add_utxos(struct chain_topology *topo, struct block *b) -{ - /* Coinbase and pegin UTXOs can be ignored */ - const uint32_t skip_features = WALLY_TX_IS_COINBASE | WALLY_TX_IS_PEGIN; - const size_t num_txs = tal_count(b->full_txs); - for (size_t i = 0; i < num_txs; i++) { - const struct bitcoin_tx *tx = b->full_txs[i]; - for (size_t n = 0; n < tx->wtx->num_outputs; n++) { - const struct wally_tx_output *output; - output = &tx->wtx->outputs[n]; - if (output->features & skip_features) - continue; - if (!is_p2wsh(output->script, output->script_len, NULL)) - continue; /* We only care about p2wsh utxos */ - - struct amount_asset amt = bitcoin_tx_output_get_amount(tx, n); - if (!amount_asset_is_main(&amt)) - continue; /* Ignore non-policy asset outputs */ - - struct bitcoin_outpoint outpoint = { b->txids[i], n }; - wallet_utxoset_add(topo->ld->wallet, &outpoint, - b->height, i, - output->script, output->script_len, - amount_asset_to_sat(&amt)); - } - } -} - -static void add_tip(struct chain_topology *topo, struct block *b) -{ - /* Attach to tip; b is now the tip. */ - assert(b->height == topo->tip->height + 1); - b->prev = topo->tip; - topo->tip->next = b; /* FIXME this doesn't seem to be used anywhere */ - topo->tip = b; - trace_span_start("wallet_block_add", b); - wallet_block_add(topo->ld->wallet, b); - trace_span_end(b); - - trace_span_start("topo_add_utxo", b); - topo_add_utxos(topo, b); - trace_span_end(b); - - trace_span_start("topo_update_spends", b); - topo_update_spends(topo, b->full_txs, b->txids, b->height); - trace_span_end(b); - - /* Only keep the transactions we care about. */ - trace_span_start("filter_block_txs", b); - filter_block_txs(topo, b); - trace_span_end(b); - - block_map_add(topo->block_map, b); -} - -static struct block *new_block(struct chain_topology *topo, - struct bitcoin_block *blk, - unsigned int height) -{ - struct block *b = tal(topo, struct block); - - bitcoin_block_blkid(blk, &b->blkid); - log_debug(topo->log, "Adding block %u: %s", - height, - fmt_bitcoin_blkid(tmpctx, &b->blkid)); - assert(!block_map_get(topo->block_map, &b->blkid)); - b->next = NULL; - b->prev = NULL; - - b->height = height; - - b->hdr = blk->hdr; - - b->full_txs = tal_steal(b, blk->tx); - b->txids = tal_steal(b, blk->txids); - - return b; -} - -static void remove_tip(struct chain_topology *topo) -{ - struct block *b = topo->tip; - const struct short_channel_id *removed_scids; - - log_debug(topo->log, "Removing stale block %u: %s", - topo->tip->height, - fmt_bitcoin_blkid(tmpctx, &b->blkid)); - - /* Move tip back one. */ - topo->tip = b->prev; - - if (!topo->tip) - fatal("Initial block %u (%s) reorganized out!", - b->height, - fmt_bitcoin_blkid(tmpctx, &b->blkid)); - - /* Grab these before we delete block from db */ - removed_scids = wallet_utxoset_get_created(tmpctx, topo->ld->wallet, - b->height); - wallet_block_remove(topo->ld->wallet, b); - - block_map_del(topo->block_map, b); - - /* These no longer exist, so gossipd drops any reference to them just - * as if they were spent. */ - gossipd_notify_spends(topo->ld, b->height, removed_scids); - tal_free(b); -} - -static void get_new_block(struct bitcoind *bitcoind, - u32 height, - struct bitcoin_blkid *blkid, - struct bitcoin_block *blk, - struct chain_topology *topo) -{ - if (!blkid && !blk) { - /* No such block, we're done. */ - updates_complete(topo); - trace_span_end(topo); - return; - } - assert(blkid && blk); - - /* Annotate all transactions with the chainparams */ - for (size_t i = 0; i < tal_count(blk->tx); i++) - blk->tx[i]->chainparams = chainparams; - - /* Unexpected predecessor? Free predecessor, refetch it. */ - if (!bitcoin_blkid_eq(&topo->tip->blkid, &blk->hdr.prev_hash)) - remove_tip(topo); - else { - add_tip(topo, new_block(topo, blk, height)); - - /* tell plugins a new block was processed */ - notify_block_added(topo->ld, topo->tip); - } - - /* Try for next one. */ - trace_span_end(topo); - try_extend_tip(topo); -} - -static void try_extend_tip(struct chain_topology *topo) -{ - topo->extend_timer = NULL; - trace_span_start("extend_tip", topo); - bitcoind_getrawblockbyheight(topo, topo->ld->bitcoind, topo->tip->height + 1, - get_new_block, topo); -} - u32 get_block_height(const struct chain_topology *topo) { - return topo->tip->height; + /* bwatch is the source of truth for processed-block height; the + * watchman holds the cached value persisted in the wallet db. */ + if (!topo->ld->watchman) + return 0; + return topo->ld->watchman->last_processed_height; } u32 get_network_blockheight(const struct chain_topology *topo) { - if (topo->tip->height > topo->headercount) - return topo->tip->height; + u32 height = get_block_height(topo); + if (height > topo->headercount) + return height; else return topo->headercount; } @@ -379,11 +75,8 @@ struct chain_topology *new_topology(struct lightningd *ld, struct logger *log) struct chain_topology *topo = tal(ld, struct chain_topology); topo->ld = ld; - topo->block_map = new_htable(topo, block_map); topo->log = log; - topo->root = NULL; topo->sync_waiters = tal(topo, struct list_head); - topo->extend_timer = NULL; topo->checkchain_timer = NULL; list_head_init(topo->sync_waiters); @@ -478,16 +171,6 @@ static void get_feerates_once(struct lightningd *ld, io_break(ld->topology); } -static void get_block_once(struct bitcoind *bitcoind, - u32 height, - struct bitcoin_blkid *blkid UNUSED, - struct bitcoin_block *blk, - struct bitcoin_block **blkp) -{ - *blkp = tal_steal(NULL, blk); - io_break(bitcoind->ld->topology); -} - /* We want to loop and poll until bitcoind has this height */ struct wait_for_height { struct bitcoind *bitcoind; @@ -533,10 +216,8 @@ void setup_topology(struct chain_topology *topo) const tal_t *local_ctx = tal(NULL, char); struct chaininfo_once *chaininfo = tal(local_ctx, struct chaininfo_once); struct feerates_once *feerates = tal(local_ctx, struct feerates_once); - struct bitcoin_block *blk; bool blockscan_start_set; u32 blockscan_start; - s64 fixup; /* This waits for bitcoind. */ bitcoind_check_commands(topo->ld->bitcoind); @@ -544,8 +225,6 @@ void setup_topology(struct chain_topology *topo) /* For testing.. */ log_debug(topo->ld->log, "All Bitcoin plugin commands registered"); - db_begin_transaction(topo->ld->wallet->db); - /*~ If we were asked to rescan from an absolute height (--rescan < 0) * then just go there. Otherwise compute the diff to our current height, * lowerbounded by 0. */ @@ -553,8 +232,8 @@ void setup_topology(struct chain_topology *topo) blockscan_start = -topo->ld->config.rescan; blockscan_start_set = true; } else { - /* Get the blockheight we are currently at, or 0 */ - blockscan_start = wallet_blocks_maxheight(topo->ld->wallet); + /* Get the blockheight bwatch reached on the previous run, or 0 */ + blockscan_start = get_block_height(topo); blockscan_start_set = (blockscan_start != 0); /* If we don't know blockscan_start, can't do this yet */ @@ -562,17 +241,6 @@ void setup_topology(struct chain_topology *topo) blockscan_start = blocknum_reduce(blockscan_start, topo->ld->config.rescan); } - fixup = db_get_intvar(topo->ld->wallet->db, "fixup_block_scan", -1); - if (fixup == -1) { - /* Never done fixup: this is set to non-zero if we have blocks. */ - topo->old_block_scan = wallet_blocks_contig_minheight(topo->ld->wallet); - db_set_intvar(topo->ld->wallet->db, "fixup_block_scan", - topo->old_block_scan); - } else { - topo->old_block_scan = fixup; - } - db_commit_transaction(topo->ld->wallet->db); - /* Sanity checks, then topology initialization. */ chaininfo->chain = NULL; feerates->rates = NULL; @@ -631,72 +299,11 @@ void setup_topology(struct chain_topology *topo) /* It's very useful to have feerates early */ update_feerates(topo->ld, feerates->feerate_floor, feerates->rates); - /* Get the first block, so we can initialize topography. */ - bitcoind_getrawblockbyheight(topo, topo->ld->bitcoind, blockscan_start, - get_block_once, &blk); - ret = io_loop_with_timers(topo->ld); - assert(ret == topo); - - tal_steal(local_ctx, blk); - topo->root = new_block(topo, blk, blockscan_start); - block_map_add(topo->block_map, topo->root); - topo->tip = topo->root; - topo->prev_tip = topo->tip->blkid; - - db_begin_transaction(topo->ld->wallet->db); - - /* In case we don't get all the way to updates_complete */ - db_set_intvar(topo->ld->wallet->db, - "last_processed_block", topo->tip->height); - - /* Rollback to the given blockheight, so we start track - * correctly again */ - wallet_blocks_rollback(topo->ld->wallet, blockscan_start); - db_commit_transaction(topo->ld->wallet->db); - tal_free(local_ctx); tal_add_destructor(topo, destroy_chain_topology); } -static void fixup_scan_block(struct bitcoind *bitcoind, - u32 height, - struct bitcoin_blkid *blkid, - struct bitcoin_block *blk, - struct chain_topology *topo) -{ - /* Can't scan the block? We will try again next restart */ - if (!blk) { - log_unusual(topo->ld->log, - "fixup_scan: could not load block %u, will retry next restart", - height); - return; - } - - log_debug(topo->ld->log, "fixup_scan: block %u with %zu txs", height, tal_count(blk->tx)); - topo_update_spends(topo, blk->tx, blk->txids, height); - - /* Caught up. */ - if (height == get_block_height(topo)) { - log_info(topo->ld->log, "Scanning for missed UTXOs finished"); - db_set_intvar(topo->ld->wallet->db, "fixup_block_scan", 0); - return; - } - - db_set_intvar(topo->ld->wallet->db, "fixup_block_scan", ++topo->old_block_scan); - bitcoind_getrawblockbyheight(topo, topo->ld->bitcoind, - topo->old_block_scan, - fixup_scan_block, topo); -} - -static void fixup_scan(struct chain_topology *topo) -{ - log_info(topo->ld->log, "Scanning for missed UTXOs from block %u", topo->old_block_scan); - bitcoind_getrawblockbyheight(topo, topo->ld->bitcoind, - topo->old_block_scan, - fixup_scan_block, topo); -} - void begin_topology(struct chain_topology *topo) { /* If we were not synced, start looping to check */ @@ -704,18 +311,14 @@ void begin_topology(struct chain_topology *topo) retry_sync(topo); /* Regular feerate updates */ start_fee_polling(topo->ld); - /* Regular block updates */ - try_extend_tip(topo); - - if (topo->old_block_scan) - fixup_scan(topo); + /* Bootstrap the rebroadcast timer; it self-perpetuates from there. */ + rebroadcast_txs(topo->ld); } void stop_topology(struct chain_topology *topo) { /* Remove timers while we're cleaning up plugins. */ tal_free(topo->checkchain_timer); - tal_free(topo->extend_timer); tal_free(topo->ld->fee_poll); topo->ld->fee_poll = NULL; } diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index 12fc4d8e9150..256f2e87f3c6 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -1,8 +1,6 @@ #ifndef LIGHTNING_LIGHTNINGD_CHAINTOPOLOGY_H #define LIGHTNING_LIGHTNINGD_CHAINTOPOLOGY_H #include "config.h" -#include -#include #include #include @@ -13,53 +11,8 @@ struct lightningd; struct peer; struct wallet; -struct block { - u32 height; - - /* Actual header. */ - struct bitcoin_block_hdr hdr; - - /* Previous block (if any). */ - struct block *prev; - - /* Next block (if any). */ - struct block *next; - - /* Key for hash table */ - struct bitcoin_blkid blkid; - - /* Full copy of txs (freed in filter_block_txs) */ - struct bitcoin_tx **full_txs; - struct bitcoin_txid *txids; -}; - -/* Hash blocks by sha */ -static inline const struct bitcoin_blkid *keyof_block_map(const struct block *b) -{ - return &b->blkid; -} - -static inline size_t hash_sha(const struct bitcoin_blkid *key) -{ - size_t ret; - - memcpy(&ret, key, sizeof(ret)); - return ret; -} - -static inline bool block_eq(const struct block *b, const struct bitcoin_blkid *key) -{ - return bitcoin_blkid_eq(&b->blkid, key); -} -HTABLE_DEFINE_NODUPS_TYPE(struct block, keyof_block_map, hash_sha, block_eq, - block_map); - struct chain_topology { struct lightningd *ld; - struct block *root; - struct block *tip; - struct bitcoin_blkid prev_tip; - struct block_map *block_map; /* Where to log things. */ struct logger *log; @@ -70,14 +23,11 @@ struct chain_topology { struct list_head *sync_waiters; /* Timers we're running. */ - struct oneshot *checkchain_timer, *extend_timer; + struct oneshot *checkchain_timer; /* The number of headers known to the bitcoin backend at startup. Not * updated after the initial check. */ u32 headercount; - - /* Progress on routine to look for old missed transactions. 0 = not interested. */ - u32 old_block_scan; }; /* Information relevant to locating a TX in a blockchain. */ @@ -112,8 +62,6 @@ void begin_topology(struct chain_topology *topo); void stop_topology(struct chain_topology *topo); -struct txlocator *locate_tx(const void *ctx, const struct chain_topology *topo, const struct bitcoin_txid *txid); - static inline bool topology_synced(const struct chain_topology *topo) { return topo->sync_waiters == NULL; diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 4660fbe643c4..93cf84493200 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -1331,19 +1331,19 @@ int main(int argc, char *argv[]) /*~ That's all of the wallet db operations for now. */ db_commit_transaction(ld->wallet->db); + /*~ Stand up the watchman: it queues bwatch RPC requests until the + * bwatch plugin reports ready, then replays them. Must come before + * setup_topology, since update_feerates() writes through + * ld->watchman, and before init_wallet_scriptpubkey_watches so the + * watches have somewhere to enqueue. */ + ld->watchman = watchman_new(ld, ld); + /*~ Initialize block topology. This does its own io_loop to * talk to bitcoind, so does its own db transactions. */ trace_span_start("setup_topology", ld->topology); 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. Must come before - * init_wallet_scriptpubkey_watches so the watches have somewhere to - * enqueue, and after setup_topology so start_block reflects the - * last-processed height. */ - ld->watchman = watchman_new(ld, ld); - trace_span_start("init_wallet_scriptpubkey_watches", ld->wallet); init_wallet_scriptpubkey_watches(ld->wallet, ld->bip32_base); trace_span_end(ld->wallet); diff --git a/lightningd/notification.c b/lightningd/notification.c index 9a5eac5041c3..7b7c5d901814 100644 --- a/lightningd/notification.c +++ b/lightningd/notification.c @@ -554,21 +554,22 @@ void notify_balance_snapshot(struct lightningd *ld, } static void block_added_notification_serialize(struct json_stream *stream, - const struct block *block) + u32 height, + const struct bitcoin_blkid *blkid) { - json_add_string(stream, "hash", - fmt_bitcoin_blkid(tmpctx, &block->blkid)); - json_add_u32(stream, "height", block->height); + json_add_string(stream, "hash", fmt_bitcoin_blkid(tmpctx, blkid)); + json_add_u32(stream, "height", height); } REGISTER_NOTIFICATION(block_added); void notify_block_added(struct lightningd *ld, - const struct block *block) + u32 height, + const struct bitcoin_blkid *blkid) { struct jsonrpc_notification *n = notify_start(ld, "block_added"); if (!n) return; - block_added_notification_serialize(n->stream, block); + block_added_notification_serialize(n->stream, height, blkid); notify_send(ld, n); } diff --git a/lightningd/notification.h b/lightningd/notification.h index 7e4c94111695..281c8a8e54ef 100644 --- a/lightningd/notification.h +++ b/lightningd/notification.h @@ -1,6 +1,7 @@ #ifndef LIGHTNING_LIGHTNINGD_NOTIFICATION_H #define LIGHTNING_LIGHTNINGD_NOTIFICATION_H #include "config.h" +#include #include #include #include @@ -99,7 +100,8 @@ void notify_balance_snapshot(struct lightningd *ld, const struct balance_snapshot *snap); void notify_block_added(struct lightningd *ld, - const struct block *block); + u32 height, + const struct bitcoin_blkid *blkid); void notify_openchannel_peer_sigs(struct lightningd *ld, const struct channel_id *cid, diff --git a/lightningd/onchain_control.h b/lightningd/onchain_control.h index 7f6d7135dbbb..b62ffb9f5d5f 100644 --- a/lightningd/onchain_control.h +++ b/lightningd/onchain_control.h @@ -5,7 +5,6 @@ struct channel; struct bitcoin_tx; -struct block; void onchaind_funding_spent(struct channel *channel, const struct bitcoin_tx *tx, diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 505803984464..8f887c34d5d4 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -1184,7 +1184,7 @@ static void htlc_accepted_hook_serialize(struct htlc_accepted_hook_payload *p, { const struct route_step *rs = p->route_step; struct htlc_in *hin = p->hin; - s32 expiry = hin->cltv_expiry, blockheight = p->ld->topology->tip->height; + s32 expiry = hin->cltv_expiry, blockheight = get_block_height(p->ld->topology); tal_free(hin->status); hin->status = diff --git a/lightningd/watchman.c b/lightningd/watchman.c index 55c54d2eb341..8bdb2971ef0c 100644 --- a/lightningd/watchman.c +++ b/lightningd/watchman.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -381,8 +382,8 @@ static void watchman_on_plugin_ready(struct lightningd *ld, struct plugin *plugi log_debug(ld->log, "bwatch reached INIT_COMPLETE, replaying pending ops (height=%u)", wm->last_processed_height); watchman_replay_pending(ld); - /* TODO: notify_block_added(ld, height, &hash) once that helper's - * signature is migrated in Group H (chaintopology removal). */ + notify_block_added(ld, wm->last_processed_height, + &wm->last_processed_hash); } } @@ -717,6 +718,9 @@ static struct command_result *json_revert_block_processed(struct command *cmd, wm->last_processed_height = *blockheight; wm->last_processed_hash = *blockhash; save_tip(wm); + /* Drop reverted blocks from the wallet table; FK ON DELETE + * CASCADE / SET NULL will clean dependents. */ + wallet_blocks_rollback(wm->ld->wallet, *blockheight); struct json_stream *response = json_stream_success(cmd); json_add_u32(response, "blockheight", *blockheight); @@ -769,8 +773,10 @@ static struct command_result *json_block_processed(struct command *cmd, wm->last_processed_height = *blockheight; wm->last_processed_hash = *blockhash; save_tip(wm); - /* TODO: notify_block_added(wm->ld, *blockheight, blockhash) once - * its signature is migrated in Group H (chaintopology removal). */ + /* Keep wallet's blocks table populated so utxoset/outputs/etc. + * FK references stay valid. */ + wallet_block_add(wm->ld->wallet, *blockheight, blockhash); + notify_block_added(wm->ld, *blockheight, blockhash); send_account_balance_snapshot(wm->ld); } diff --git a/wallet/reservation.c b/wallet/reservation.c index c28a52a875a5..b216ba379239 100644 --- a/wallet/reservation.c +++ b/wallet/reservation.c @@ -434,9 +434,9 @@ static inline u32 minconf_to_maxheight(u32 minconf, struct lightningd *ld) /* Avoid wrapping around and suddenly allowing any confirmed * outputs. Since we can't have a coinbase output, and 0 is taken for * the disable case, we can just clamp to 1. */ - if (minconf >= ld->topology->tip->height) + if (minconf >= get_block_height(ld->topology)) return 1; - return ld->topology->tip->height - minconf + 1; + return get_block_height(ld->topology) - minconf + 1; } /* Returns false if it needed to create change, but couldn't afford. */ diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 3b5acf4d0db7..04125af962b3 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -957,7 +957,8 @@ static bool test_wallet_outputs(struct lightningd *ld, const tal_t *ctx, bool bi struct pubkey pk; struct node_id id; struct wireaddr_internal addr; - struct block block; + u32 block_height; + struct bitcoin_blkid block_hash; struct channel channel; struct utxo *one_utxo; const struct utxo **utxos; @@ -1052,10 +1053,9 @@ static bool test_wallet_outputs(struct lightningd *ld, const tal_t *ctx, bool bi u32 *blockheight = tal(w, u32); *blockheight = 100; /* We gotta add a block to the database though */ - memset(&block, 0, sizeof(block)); - block.height = 100; - memset(&block.blkid, 2, sizeof(block.blkid)); - wallet_block_add(w, &block); + block_height = 100; + memset(&block_hash, 2, sizeof(block_hash)); + wallet_block_add(w, block_height, &block_hash); CHECK_MSG(!wallet_err, wallet_err); u.blockheight = blockheight; diff --git a/wallet/wallet.c b/wallet/wallet.c index 8e6bfd1b5ee0..a8489a3c8047 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -2577,46 +2577,6 @@ void wallet_channel_stats_incr_out_fulfilled(struct wallet *w, u64 id, wallet_channel_stats_incr_x(w, id, m, query); } -u32 wallet_blocks_maxheight(struct wallet *w) -{ - u32 max = 0; - struct db_stmt *stmt = db_prepare_v2(w->db, SQL("SELECT MAX(height) FROM blocks;")); - db_query_prepared(stmt); - - /* If we ever processed a block we'll get the latest block in the chain */ - if (db_step(stmt)) { - if (!db_col_is_null(stmt, "MAX(height)")) { - max = db_col_int(stmt, "MAX(height)"); - } else { - db_col_ignore(stmt, "MAX(height)"); - } - } - tal_free(stmt); - return max; -} - -u32 wallet_blocks_contig_minheight(struct wallet *w) -{ - u32 min = 0; - struct db_stmt *stmt = db_prepare_v2(w->db, SQL("SELECT MAX(b.height)" - " FROM blocks b" - " WHERE NOT EXISTS (" - " SELECT 1" - " FROM blocks b2" - " WHERE b2.height = b.height - 1)")); - db_query_prepared(stmt); - - /* If we ever processed a block we'll get the first block in - * the last run of blocks */ - if (db_step(stmt)) { - if (!db_col_is_null(stmt, "MAX(b.height)")) { - min = db_col_int(stmt, "MAX(b.height)"); - } - } - tal_free(stmt); - return min; -} - static void wallet_channel_config_insert(struct wallet *w, struct channel_config *cc) { @@ -4907,39 +4867,20 @@ void wallet_utxoset_prune(struct wallet *w, u32 blockheight) db_exec_prepared_v2(take(stmt)); } -void wallet_block_add(struct wallet *w, struct block *b) +void wallet_block_add(struct wallet *w, u32 height, + const struct bitcoin_blkid *blkid) { + /* Idempotent: bwatch may resend a block_processed during replay. */ + if (wallet_have_block(w, height)) + return; + struct db_stmt *stmt = db_prepare_v2(w->db, SQL("INSERT INTO blocks " "(height, hash, prev_hash) " - "VALUES (?, ?, ?);")); - db_bind_int(stmt, b->height); - db_bind_sha256d(stmt, &b->blkid.shad); - if (b->prev) { - db_bind_sha256d(stmt, &b->prev->blkid.shad); - } else { - db_bind_null(stmt); - } - db_exec_prepared_v2(take(stmt)); -} - -void wallet_block_remove(struct wallet *w, struct block *b) -{ - struct db_stmt *stmt = - db_prepare_v2(w->db, SQL("DELETE FROM blocks WHERE hash = ?")); - db_bind_sha256d(stmt, &b->blkid.shad); + "VALUES (?, ?, NULL);")); + db_bind_int(stmt, height); + db_bind_sha256d(stmt, &blkid->shad); db_exec_prepared_v2(take(stmt)); - - /* Make sure that all descendants of the block are also deleted */ - stmt = db_prepare_v2(w->db, - SQL("SELECT * FROM blocks WHERE height >= ?;")); - db_bind_int(stmt, b->height); - db_query_prepared(stmt); - assert(!db_step(stmt)); - tal_free(stmt); - - /* We might need to watch more now-unspent UTXOs */ - refill_outpointfilters(w); } void wallet_blocks_rollback(struct wallet *w, u32 height) diff --git a/wallet/wallet.h b/wallet/wallet.h index 495370ab57e3..673fb3dedf7e 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -791,25 +791,6 @@ void wallet_channel_stats_incr_in_fulfilled(struct wallet *w, u64 cdbid, struct void wallet_channel_stats_incr_out_offered(struct wallet *w, u64 cdbid, struct amount_msat msatoshi); void wallet_channel_stats_incr_out_fulfilled(struct wallet *w, u64 cdbid, struct amount_msat msatoshi); -/** - * Retrieve the blockheight of the last block processed by lightningd. - * - * Will return the 0 if the wallet was never used before. - * - * @w: wallet to load from. - */ -u32 wallet_blocks_maxheight(struct wallet *w); - -/** - * Retrieve the blockheight of the first block processed by lightningd (ignoring - * backfilled blocks for gossip). - * - * Will return the 0 if the wallet was never used before. - * - * @w: wallet to load from. - */ -u32 wallet_blocks_contig_minheight(struct wallet *w); - /** * wallet_extract_owned_outputs - given a tx, extract all of our outputs * @w: wallet @@ -1199,14 +1180,14 @@ void wallet_htlc_sigs_add(struct wallet *w, u64 channel_id, bool wallet_sanity_check(struct wallet *w); /** - * wallet_block_add - Add a block to the blockchain tracked by this wallet - */ -void wallet_block_add(struct wallet *w, struct block *b); - -/** - * wallet_block_remove - Remove a block (and all its descendants) from the tracked blockchain + * wallet_block_add - Record a block in the wallet's blocks table. + * + * Bwatch is the source of truth for the chain; this just keeps the wallet's + * `blocks` table populated so the FK references on outputs / utxoset / + * channeltxs / transactions stay valid. */ -void wallet_block_remove(struct wallet *w, struct block *b); +void wallet_block_add(struct wallet *w, u32 height, + const struct bitcoin_blkid *blkid); /** * wallet_blocks_rollback - Roll the blockchain back to the given height