lightningd: use scriptpubkey watch and block depth watch for splicing.
What changed, and why it matters
This change rewrites how Core Lightning watches for splice-inflight funding transactions. Previously it watched for a specific transaction ID. Now it watches for any transaction that creates an output matching the expected 2-of-2 multisig script (scriptpubkey) and then tracks how deeply that block is buried. The intent appears to be robustness—so a splice still works even if the funding transaction ID changes before confirmation—but the change also removes some defensive error handling and replaces it with an abort() and a new reorg callback. There is no explicit security claim in the commit, and no external advisory is supplied.
Review whether abort() in splice_depth_cb is safe against a maliciously crafted or reorg-induced inconsistent wallet state; consider whether the scriptpubkey watch could match unintended transactions and whether total_funds matching is sufficient to prevent confusion. No immediate patch or CVE is indicated by the supplied materials, but the change alters failure semantics and should be included in normal release testing for splicing correctness.
Security signals we found
Change from txid-based watch to scriptpubkey-based watch for splice funding detection
Removal of channel_fail_permanent on invalid SCID derivation in depth callback; replaced with abort()
Addition of separate reorg callback (splice_reorged_cb) returning DELETE_WATCH
Reduced log verbosity for non-AWAITING_SPLICE splice events (log_info -> log_debug)
New use of bitcoin_redeem_2of2 and P2WSH scriptpubkey for splice funding watch
Evidence from the diff
The patch refactors splice confirmation tracking in lightningd/channel_control.c. The old splice_depth_cb was registered via watch_txid on the funding txid and derived the short_channel_id from the located transaction. The new code splits responsibilities: watch_scriptpubkey looks for any transaction output paying the 2-of-2 P2WSH funding script; splice_found derives the SCID and registers a block-depth watch; splice_depth_cb now only forwards depth updates to channeld; a separate splice_reorged_cb handles reorg removal. Notable changes: the previous permanent channel-fail path for invalid SCID derivation is replaced by abort() in splice_depth_cb; logging levels are reduced from log_info to log_debug in some paths; the txid parameter is removed from the depth callback signature. The commit message frames this as ‘use scriptpubkey watch and block depth watch for splicing’ with no security rationale.
Changed components
lightningd/channel_control.csplice inflight confirmation logictransaction/scriptpubkey watch subsystemblock-depth watch subsystemInspect captured patch +60 / −33
diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c
index 836c944d..0da795ee 100644
--- a/lightningd/channel_control.c
+++ b/lightningd/channel_control.c
@@ -686,50 +686,34 @@ static void handle_splice_confirmed_signed(struct lightningd *ld,
}
static enum watch_result splice_depth_cb(struct lightningd *ld,
- const struct bitcoin_txid *txid,
- const struct bitcoin_tx *tx,
unsigned int depth,
struct channel_inflight *inflight)
{
- /* find_txwatch triggers a type warning on inflight, so we do this. */
- struct txlocator *loc;
- struct short_channel_id scid;
-
- /* What scid is this giving us? */
- loc = wallet_transaction_locate(tmpctx, ld->wallet, txid);
- if (!mk_short_channel_id(&scid,
- loc->blkheight, loc->index,
- inflight->funding->outpoint.n)) {
- channel_fail_permanent(inflight->channel,
- REASON_LOCAL,
- "Invalid funding scid %u:%u:%u",
- loc->blkheight, loc->index,
- inflight->funding->outpoint.n);
- return false;
- }
-
/* 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_info(inflight->channel->log, "Splice inflight event but not"
- " in AWAITING_SPLICE, ending watch of txid %s",
- fmt_bitcoin_txid(tmpctx, txid));
+ 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;
}
- /* Reorged out? OK, we're not committed yet. */
- if (depth == 0) {
- return KEEP_WATCHING;
- }
-
if (inflight->channel->owner) {
- log_info(inflight->channel->log, "splice_depth_cb: sending funding depth scid: %s",
- fmt_short_channel_id(tmpctx, scid));
+ /* FIXME: Save this in inflight so we don't neeed to re-derive. */
+ struct short_channel_id scid;
+ struct txlocator *loc = wallet_transaction_locate(tmpctx, ld->wallet, &inflight->funding->outpoint.txid);
+ if (!mk_short_channel_id(&scid,
+ loc->blkheight, loc->index,
+ inflight->funding->outpoint.n))
+ abort();
+ log_debug(inflight->channel->log, "splice_depth_cb: sending funding depth scid: %s",
+ fmt_short_channel_id(tmpctx, scid));
subd_send_msg(inflight->channel->owner,
take(towire_channeld_funding_depth(
NULL, &scid,
- depth, true, txid)));
+ depth, true,
+ &inflight->funding->outpoint.txid)));
}
/* channeld will tell us when splice is locked in: we'll clean
@@ -737,15 +721,58 @@ static enum watch_result splice_depth_cb(struct lightningd *ld,
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));
+ 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)
+{
+ struct short_channel_id scid;
+
+ if (!mk_short_channel_id(&scid,
+ loc->blkheight, loc->index,
+ inflight->funding->outpoint.n)) {
+ 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);
+}
+
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_txid(inflight, ld->topology,
- &inflight->funding->outpoint.txid,
- splice_depth_cb, inflight);
+
+ watch_scriptpubkey(inflight, ld->topology,
+ take(scriptpubkey_p2wsh(NULL, funding_wscript)),
+ &inflight->funding->outpoint,
+ inflight->funding->total_funds,
+ splice_found,
+ inflight);
}
static void handle_splice_sending_sigs(struct lightningd *ld,
Why this scored 31/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.