lightningd: use scriptpubkey, not watch_txid for initial spotting of the funding tx.
What changed, and why it matters
This change alters how Core Lightning first detects a channel's funding transaction on the Bitcoin blockchain. Previously it looked for an exact transaction ID; now it watches for any transaction that pays to the channel's expected 2-of-2 multisig script. The commit message frames this as a refactor of the three existing cases handled by the funding-depth callback, not as a security fix. There is no direct evidence in the diff of a vulnerability being patched, but the change could reduce sensitivity to transaction malleability or to cases where the funding transaction ID is not yet known.
Review the watch_scriptpubkey_ implementation to confirm it correctly validates both the expected outpoint and amount before invoking channel_funding_found, and ensure the interaction between the new scriptpubkey watch and the existing txid watch does not miss reorgs or double-trigger. Treat as a routine refactor unless additional context shows it fixes a known issue.
Security signals we found
Changes blockchain watching logic for funding transactions
Replaces txid-based initial detection with scriptpubkey-based detection
No explicit security claim in commit message or diff
Could affect behavior under transaction malleability or reorg scenarios
Evidence from the diff
The patch introduces channel_funding_found(), triggered by a new scriptpubkey watch (watch_scriptpubkey_), to handle the first sighting of a funding output. The existing funding_depth_cb() continues to handle reorgs and depth increases via watch_txid. The new watch is registered with the P2WSH of the 2-of-2 funding redeem script, the expected outpoint, and the expected amount. This decouples initial funding detection from the exact txid, which is useful when the txid may differ (e.g., malleated signatures, collaborative transactions, or splicing). The change removes a wallet_transaction_locate() call from funding_depth_cb() for the initial case and moves it into the new callback. Test stubs are updated accordingly.
Changed components
lightningd/peer_control.clightningd/test/run-invoice-select-inchan.cwallet/test/run-wallet.cInspect captured patch +47 / −10
diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c
index 52247378..efda167b 100644
--- a/lightningd/peer_control.c
+++ b/lightningd/peer_control.c
@@ -2307,14 +2307,23 @@ void update_channel_from_inflight(struct lightningd *ld,
wallet_channel_save(ld->wallet, 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)
+{
+ /* Closes channel if it doesn't fit in an scid! */
+ depthcb_update_scid(channel, &channel->funding, loc);
+}
+
static enum watch_result funding_depth_cb(struct lightningd *ld,
const struct bitcoin_txid *txid,
const struct bitcoin_tx *tx,
unsigned int depth,
struct channel *channel)
{
- struct txlocator *loc;
-
/* This is stub channel, we don't activate anything! */
if (channel->scid && is_stub_scid(*channel->scid))
return DELETE_WATCH;
@@ -2384,10 +2393,6 @@ static enum watch_result funding_depth_cb(struct lightningd *ld,
return KEEP_WATCHING;
}
- loc = wallet_transaction_locate(tmpctx, ld->wallet, &channel->funding.txid);
- if (!depthcb_update_scid(channel, &channel->funding, loc))
- return DELETE_WATCH;
-
switch (channel->state) {
/* We should not be in the callback! */
case DUALOPEND_AWAITING_LOCKIN:
@@ -2483,10 +2488,20 @@ void channel_unwatch_funding(struct lightningd *ld, struct channel *channel)
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);
+
log_debug(channel->log, "Watching for funding txid: %s",
fmt_bitcoin_txid(tmpctx, &channel->funding.txid));
watch_txid(channel, ld->topology,
&channel->funding.txid, funding_depth_cb, channel);
+ watch_scriptpubkey(channel, ld->topology,
+ take(scriptpubkey_p2wsh(NULL, funding_wscript)),
+ &channel->funding,
+ channel->funding_sats,
+ channel_funding_found,
+ channel);
tal_free(channel->funding_spend_watch);
channel->funding_spend_watch = watch_txo(channel, ld->topology, channel,
diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c
index bb27f087..2aff2dfd 100644
--- a/lightningd/test/run-invoice-select-inchan.c
+++ b/lightningd/test/run-invoice-select-inchan.c
@@ -727,14 +727,23 @@ void wallet_transaction_add(struct wallet *w UNNEEDED, const struct wally_tx *tx
/* Generated stub for wallet_transaction_height */
u32 wallet_transaction_height(struct wallet *w UNNEEDED, const struct bitcoin_txid *txid UNNEEDED)
{ fprintf(stderr, "wallet_transaction_height called!\n"); abort(); }
-/* Generated stub for wallet_transaction_locate */
-struct txlocator *wallet_transaction_locate(const tal_t *ctx UNNEEDED, struct wallet *w UNNEEDED,
- const struct bitcoin_txid *txid UNNEEDED)
-{ fprintf(stderr, "wallet_transaction_locate 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,
+ 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 watch_splice_inflight */
void watch_splice_inflight(struct lightningd *ld UNNEEDED,
struct channel_inflight *inflight UNNEEDED)
diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c
index 4f698c34..65553af4 100644
--- a/wallet/test/run-wallet.c
+++ b/wallet/test/run-wallet.c
@@ -773,6 +773,19 @@ u8 *unsigned_node_announcement(const tal_t *ctx UNNEEDED,
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,
+ 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 watch_splice_inflight */
void watch_splice_inflight(struct lightningd *ld UNNEEDED,
struct channel_inflight *inflight UNNEEDED)
Why this scored 24/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.