wallet: register scriptpubkey watches at startup
What changed, and why it matters
This change makes Core Lightning register long-lasting 'watches' for all wallet deposit addresses when the node starts up, so the new bwatch plugin can spot incoming payments from the first block it scans. It also removes a now-redundant per-transaction watch on unconfirmed change outputs. The patch is a functional improvement for the experimental bwatch feature, not a clear-cut security fix, but a missing or incomplete watch could in principle cause deposits to be overlooked.
Treat as a normal code review item for the experimental bwatch feature. Verify that init_wallet_scriptpubkey_watches() covers every address form wallet_can_spend() recognizes, that the keyscan_gap lookahead is sufficient, and that removing the unconfirmed-change watch does not create a window where a confirming change output is missed before the per-key watch is active. No urgent security action is indicated by the commit alone.
Security signals we found
Change is part of an experimental chain-watcher (bwatch) feature
Adds startup registration of scriptPubKey watches for all derived wallet keys
Removes a redundant unconfirmed-change watch, relying on the new blanket coverage
Uses UINT32_MAX start_block to avoid rescan, depending on bwatch polling for catch-up
No explicit security framing, CVE, or vulnerability description in commit message
Evidence from the diff
The commit introduces init_wallet_scriptpubkey_watches(), which iterates every derived HD key (BIP32 and BIP86) up to the stored max index plus keyscan_gap and registers a bwatch scriptPubKey watch for each address form. wallet_get_newindex() now also calls watch_newindex_scripts() for freshly derived keys. The per-UTXO scriptPubKey watch for unconfirmed change in bwatch_got_utxo() is removed because the per-key watch already covers it. start_block is set to UINT32_MAX to avoid a startup rescan; catch-up relies on bwatch polling.
Changed components
lightningd startup sequencewallet/wallet.cwallet/wallet.hbwatch plugin integrationHD key address derivation and watch registrationInspect captured patch +89 / −11
diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c
index 682900c..44ed3fa 100644
--- a/lightningd/lightningd.c
+++ b/lightningd/lightningd.c
@@ -1367,17 +1367,29 @@ int main(int argc, char *argv[])
trace_span_end(ld->topology);
/*~ Stand up the watchman: it queues bwatch RPC requests until the
- * bwatch plugin reports ready, then replays them. Must come after
- * setup_topology so start_block reflects the last-processed height.
+ * 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.
*
* bwatch is opt-in for now: the --experimental-bwatch flag is
* registered by the bwatch plugin, so peek at the configvar to gate
* the lightningd side too. Without it, ld->watchman stays NULL and
* the watchman_* entry points are no-ops, leaving chain_topology as
* the only chain watcher. */
- if (bwatch_enabled(ld))
+ if (bwatch_enabled(ld)) {
ld->watchman = watchman_new(ld, ld);
+ /*~ Reads intvars and the addresses table, so needs a
+ * transaction (the datastore writes it triggers self-wrap
+ * when necessary). */
+ db_begin_transaction(ld->wallet->db);
+ trace_span_start("init_wallet_scriptpubkey_watches", ld->wallet);
+ init_wallet_scriptpubkey_watches(ld->wallet);
+ trace_span_end(ld->wallet);
+ db_commit_transaction(ld->wallet->db);
+ }
+
db_begin_transaction(ld->wallet->db);
trace_span_start("delete_old_htlcs", ld->wallet);
wallet_delete_old_htlcs(ld->wallet);
diff --git a/lightningd/test/run-find_my_abspath.c b/lightningd/test/run-find_my_abspath.c
index fc37822..ffd6cbe 100644
--- a/lightningd/test/run-find_my_abspath.c
+++ b/lightningd/test/run-find_my_abspath.c
@@ -92,6 +92,9 @@ 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)
+{ 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(); }
diff --git a/wallet/wallet.c b/wallet/wallet.c
index 0b12d37..5d54491 100644
--- a/wallet/wallet.c
+++ b/wallet/wallet.c
@@ -1011,6 +1011,43 @@ bool wallet_can_spend(struct wallet *w, const u8 *script, size_t script_len,
return true;
}
+static void watch_newindex_scripts(struct lightningd *ld,
+ u64 keyidx,
+ enum addrtype addrtype)
+{
+ struct pubkey pubkey;
+ const u8 *scriptpubkey;
+ bool legacy = (ld->bip86_base == NULL);
+ u32 start_block = UINT32_MAX;
+
+ if (ld->bip86_base)
+ bip86_pubkey(ld, &pubkey, keyidx);
+ else
+ bip32_pubkey(ld, &pubkey, keyidx);
+
+ if (addrtype & ADDR_BECH32) {
+ scriptpubkey = scriptpubkey_p2wpkh(tmpctx, &pubkey);
+ wallet_add_bwatch_scriptpubkey(ld, keyidx, start_block,
+ scriptpubkey,
+ tal_bytelen(scriptpubkey));
+
+ /* Pre-24.11 ADDR_ALL can include legacy wrapped form. */
+ if (addrtype == ADDR_ALL && legacy) {
+ const u8 *p2sh = scriptpubkey_p2sh(tmpctx, scriptpubkey);
+ wallet_add_bwatch_scriptpubkey(ld, keyidx, start_block,
+ p2sh,
+ tal_bytelen(p2sh));
+ }
+ }
+
+ if (addrtype & ADDR_P2TR) {
+ scriptpubkey = scriptpubkey_p2tr(tmpctx, &pubkey);
+ wallet_add_bwatch_scriptpubkey(ld, keyidx, start_block,
+ scriptpubkey,
+ tal_bytelen(scriptpubkey));
+ }
+}
+
s64 wallet_get_newindex(struct lightningd *ld, enum addrtype addrtype)
{
struct db_stmt *stmt;
@@ -1034,6 +1071,8 @@ s64 wallet_get_newindex(struct lightningd *ld, enum addrtype addrtype)
db_bind_int(stmt, wallet_addrtype_in_db(addrtype));
db_exec_prepared_v2(take(stmt));
+ watch_newindex_scripts(ld, newidx, addrtype);
+
return newidx;
}
@@ -8033,6 +8072,33 @@ void wallet_add_bwatch_scriptpubkey(struct lightningd *ld,
script, script_len, start_block);
}
+/* Register perennial bwatch watches for every scriptpubkey wallet_can_spend()
+ * recognizes, including keyscan_gap lookahead. UINT32_MAX avoids a per-key
+ * startup rescan; existing outputs come from migration and catch-up comes from
+ * bwatch polling. */
+void init_wallet_scriptpubkey_watches(struct wallet *w)
+{
+ struct wallet_address_htable_iter it;
+ u64 bip32_max_index = db_get_intvar(w->db, "bip32_max_index", 0);
+ u64 bip86_max_index = db_get_intvar(w->db, "bip86_max_index", 0);
+ u64 max_index = bip32_max_index > bip86_max_index
+ ? bip32_max_index : bip86_max_index;
+ u32 start_block = UINT32_MAX;
+
+ /* Extend the table to cover every key we've derived plus lookahead;
+ * same rule wallet_can_spend applies. */
+ while (w->our_addresses_maxindex < max_index + w->keyscan_gap)
+ our_addresses_add_for_index(w, ++w->our_addresses_maxindex);
+
+ for (struct wallet_address *waddr
+ = wallet_address_htable_first(w->our_addresses, &it);
+ waddr;
+ waddr = wallet_address_htable_next(w->our_addresses, &it)) {
+ wallet_add_bwatch_scriptpubkey(w->ld, waddr->index, start_block,
+ waddr->swl.script, waddr->swl.len);
+ }
+}
+
/* 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
@@ -8223,14 +8289,6 @@ static void bwatch_got_utxo(struct wallet *w,
watchman_watch_outpoint(w->ld,
owner_wallet_utxo(tmpctx, &output->outpoint),
&output->outpoint, start_block);
-
- /* Unconfirmed: keep watching the scriptpubkey so bwatch tells us when
- * the output confirms. UINT32_MAX = perennial watch: never skip on
- * reorg, never rescan. */
- if (!blockheight)
- wallet_add_bwatch_scriptpubkey(w->ld, keyindex, UINT32_MAX,
- output->txout->script,
- output->txout->script_len);
}
/* A wallet/spk watch owner suffix is "<keyindex>/<form>", as written by
diff --git a/wallet/wallet.h b/wallet/wallet.h
index 366a558..607fa8d 100644
--- a/wallet/wallet.h
+++ b/wallet/wallet.h
@@ -2098,4 +2098,9 @@ void wallet_add_bwatch_scriptpubkey(struct lightningd *ld,
const u8 *script,
size_t script_len);
+/* Register a bwatch watch for every scriptpubkey wallet_can_spend
+ * recognizes: every HD key up through {bip32,bip86}_max_index plus the
+ * keyscan_gap lookahead, in the address forms actually issued. */
+void init_wallet_scriptpubkey_watches(struct wallet *w);
+
#endif /* LIGHTNING_WALLET_WALLET_H */
Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.