From ca2b7091b575f7d1b044223a12a076e4eb8db4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Wed, 24 Jun 2026 17:54:23 -0300 Subject: [PATCH 1/6] splice: add tests for funding_tx_index tracking Add regression tests that each funding transaction carries a funding_tx_index (0 for the original funding, +1 per splice), that it is persisted and reloaded across restart, and that a pending splice inflight carries its index. Changelog-None --- tests/test_splicing_disconnect.py | 116 ++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/tests/test_splicing_disconnect.py b/tests/test_splicing_disconnect.py index a0975b0b4a10..5a2ffb8a99ea 100644 --- a/tests/test_splicing_disconnect.py +++ b/tests/test_splicing_disconnect.py @@ -66,6 +66,57 @@ def test_splice_disconnect_sig(node_factory, bitcoind): assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0 +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +@pytest.mark.xfail(strict=True, reason="funding_tx_index tracking not yet implemented") +def test_splice_reconnect_after_lock_no_channel_ready(node_factory, bitcoind): + # Once a splice locks, channel funding txid is updated to the splice txid. + # On reconnect we must still recognise the peer's `my_current_funding_locked` + # as a splice (funding_tx_index > 0) and NOT retransmit `channel_ready`. + # channeld used to compare the txid against the (already-updated) channel + # funding txid, which is wrong once a splice completes. This drives a full + # splice + restart + reestablish to exercise the funding_tx_index path end + # to end (DB columns, inflight wire, and the reestablish detection). + l1 = node_factory.get_node(may_reconnect=True) + l2 = node_factory.get_node(may_reconnect=True) + l1.openchannel(l2, 1000000) + + chan_id = l1.get_channel_id(l2) + + l1.rpc.splicein(chan_id, "100000") + + # Confirm and lock the splice on both sides. + bitcoind.generate_block(6, wait_for_mempool=1) + l1.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + l2.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + + # Restart l1 so it reloads channel + inflight state from the DB (exercising + # the new funding_tx_index columns) and reconnects/reestablishes. + l1.restart() + + # Force the reconnect rather than waiting on auto-reconnect backoff. + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + + l1.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_REESTABLISH') + l2.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_REESTABLISH') + + # The locked splice must have been persisted with funding_tx_index == 1... + rows = l1.db_query("SELECT funding_tx_index FROM channels;") + assert max(r['funding_tx_index'] for r in rows) == 1 + + # Drive a payment so both peers finish reestablish; this guarantees any + # erroneous channel_ready retransmit is already logged before we assert it + # did not happen. + inv = l2.rpc.invoice(10**2, 'lbl', 'desc') + l1.rpc.xpay(inv['bolt11']) + + # ...and the splice was recognised on reestablish, so channel_ready is NOT + # retransmitted. + assert not l1.daemon.is_in_log(r'Retransmitting channel_ready') + assert not l2.daemon.is_in_log(r'Retransmitting channel_ready') + + @pytest.mark.openchannel('v1') @pytest.mark.openchannel('v2') @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') @@ -114,3 +165,68 @@ def test_splice_disconnect_commit(node_factory, bitcoind, executor): # Check that the splice doesn't generate a unilateral close transaction time.sleep(5) assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0 + + +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +@pytest.mark.xfail(strict=True, reason="funding_tx_index tracking not yet implemented") +def test_splice_funding_tx_index_increments(node_factory, bitcoind): + # funding_tx_index is 0 for the original funding and increments by 1 per + # splice. Two sequential splices must reach index 2 on the persisted + # channel (exercises the parent + 1 assignment). + l1 = node_factory.get_node(may_reconnect=True) + l2 = node_factory.get_node(may_reconnect=True) + l1.openchannel(l2, 1000000) + chan_id = l1.get_channel_id(l2) + + def do_splice(amount): + l1.rpc.splicein(chan_id, str(amount)) + bitcoind.generate_block(6, wait_for_mempool=1) + l1.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + l2.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + + def channel_funding_tx_index(): + rows = l1.db_query("SELECT funding_tx_index FROM channels;") + return max(r['funding_tx_index'] for r in rows) + + # First splice: 0 -> 1 + do_splice(100000) + assert channel_funding_tx_index() == 1 + + # Second splice: 1 -> 2 + do_splice(50000) + assert channel_funding_tx_index() == 2 + + +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +@pytest.mark.xfail(strict=True, reason="funding_tx_index tracking not yet implemented") +def test_splice_inflight_funding_tx_index(node_factory, bitcoind): + # A pending (not-yet-locked) splice inflight carries funding_tx_index == 1 + # (the open was index 0), and the value must survive a restart so the + # reestablish detection still has it. + l1 = node_factory.get_node(may_reconnect=True) + l2 = node_factory.get_node(may_reconnect=True) + l1.openchannel(l2, 1000000) + chan_id = l1.get_channel_id(l2) + + l1.rpc.splicein(chan_id, "100000") + + # The pending splice inflight is index 1. + inflights = l1.db_query("SELECT funding_tx_index FROM" + " channel_funding_inflights;") + assert [r['funding_tx_index'] for r in inflights] == [1] + + # Restart l1: the inflight (and its index) must reload from the DB. + l1.restart() + l1.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_REESTABLISH') + inflights = l1.db_query("SELECT funding_tx_index FROM" + " channel_funding_inflights;") + assert [r['funding_tx_index'] for r in inflights] == [1] + + # And the splice still completes after the restart. + bitcoind.generate_block(6, wait_for_mempool=1) + l1.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + l2.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') From b489644f73003e86c825cf554b68de6097f719d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Wed, 24 Jun 2026 18:13:53 -0300 Subject: [PATCH 2/6] splice: track funding_tx_index on inflights and channel funding Give every funding tx a stable index: 0 for the original funding (including RBF attempts), incrementing by 1 per splice. Threaded through the channeld inflight, the channel, the channeld_init and channeld_add_inflight wire messages, set on splice creation and carried onto the channel funding when a splice locks, and persisted in the channel_funding_inflights and channels tables. --- channeld/channeld.c | 10 ++++++++++ channeld/channeld_wire.csv | 2 ++ channeld/full_channel.c | 2 ++ channeld/full_channel.h | 1 + channeld/inflight.c | 2 ++ channeld/inflight.h | 3 +++ channeld/test/run-full_channel.c | 4 ++-- common/initial_channel.c | 4 ++++ common/initial_channel.h | 7 +++++++ devtools/mkcommit.c | 2 +- lightningd/channel.c | 5 ++++- lightningd/channel.h | 8 ++++++++ lightningd/channel_control.c | 5 +++++ lightningd/dual_open_control.c | 2 ++ lightningd/opening_control.c | 2 ++ lightningd/peer_control.c | 1 + openingd/dualopend.c | 4 ++++ openingd/openingd.c | 2 ++ tests/fuzz/fuzz-full_channel.c | 4 ++-- tests/fuzz/fuzz-initial_channel.c | 3 ++- tests/plugins/channeld_fakenet.c | 3 +++ tests/test_splicing_disconnect.py | 4 +--- wallet/migrations.c | 6 ++++++ wallet/test/run-chain_moves_duplicate-detect.c | 2 ++ wallet/test/run-db.c | 2 ++ .../test/run-migrate_remove_chain_moves_duplicates.c | 2 ++ wallet/test/run-wallet.c | 5 +++-- wallet/wallet.c | 10 +++++++++- 28 files changed, 94 insertions(+), 13 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index 3848f0d2e837..b523463b2570 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -483,6 +483,7 @@ static void check_mutual_splice_locked(struct peer *peer) fmt_channel(tmpctx, peer->channel)); error = channel_update_funding(peer->channel, &inflight->outpoint, + inflight->funding_tx_index, inflight->amnt, inflight->splice_amnt); if (error) @@ -4362,6 +4363,7 @@ static void splice_accepter(struct peer *peer, const u8 *inmsg) &peer->splicing->remote_funding_pubkey, &outpoint.txid, outpoint.n, + peer->channel->funding_tx_index + 1, funding_feerate_perkw, both_amount, peer->splicing->accepter_relative, @@ -4379,6 +4381,8 @@ static void splice_accepter(struct peer *peer, const u8 *inmsg) &new_inflight->outpoint.txid, NULL); new_inflight->remote_funding = peer->splicing->remote_funding_pubkey; new_inflight->outpoint = outpoint; + /* A splice's funding tx is the parent funding's index + 1. */ + new_inflight->funding_tx_index = peer->channel->funding_tx_index + 1; new_inflight->amnt = both_amount; new_inflight->psbt = clone_psbt(new_inflight, ictx->current_psbt); new_inflight->splice_amnt = peer->splicing->accepter_relative; @@ -4657,6 +4661,7 @@ static void splice_initiator_user_finalized(struct peer *peer) &peer->splicing->remote_funding_pubkey, ¤t_psbt_txid, chan_output_index, + peer->channel->funding_tx_index + 1, peer->splicing->feerate_per_kw, amount_sat(new_chan_output->amount), peer->splicing->opener_relative, @@ -4674,6 +4679,8 @@ static void splice_initiator_user_finalized(struct peer *peer) NULL); new_inflight->remote_funding = peer->splicing->remote_funding_pubkey; new_inflight->outpoint.n = chan_output_index; + /* A splice's funding tx is the parent funding's index + 1. */ + new_inflight->funding_tx_index = peer->channel->funding_tx_index + 1; new_inflight->amnt = amount_sat(new_chan_output->amount); new_inflight->splice_amnt = peer->splicing->opener_relative; new_inflight->last_tx = NULL; @@ -6830,6 +6837,7 @@ static void init_channel(struct peer *peer) { struct basepoints points[NUM_SIDES]; struct amount_sat funding_sats; + u32 funding_tx_index; struct amount_msat local_msat; struct pubkey funding_pubkey[NUM_SIDES]; struct channel_config conf[NUM_SIDES]; @@ -6859,6 +6867,7 @@ static void init_channel(struct peer *peer) &peer->channel_id, &funding, &funding_sats, + &funding_tx_index, &minimum_depth, &peer->our_blockheight, &blockheight_states, @@ -6973,6 +6982,7 @@ static void init_channel(struct peer *peer) peer->channel = new_full_channel(peer, &peer->channel_id, &funding, + funding_tx_index, minimum_depth, take(blockheight_states), lease_expiry, diff --git a/channeld/channeld_wire.csv b/channeld/channeld_wire.csv index 208da3038edb..b98423ca4d04 100644 --- a/channeld/channeld_wire.csv +++ b/channeld/channeld_wire.csv @@ -21,6 +21,7 @@ msgdata,channeld_init,hsm_capabilities,u32,num_hsm_capabilities msgdata,channeld_init,channel_id,channel_id, msgdata,channeld_init,funding,bitcoin_outpoint, msgdata,channeld_init,funding_satoshi,amount_sat, +msgdata,channeld_init,funding_tx_index,u32, msgdata,channeld_init,minimum_depth,u32, msgdata,channeld_init,our_blockheight,u32, msgdata,channeld_init,blockheight_states,height_states, @@ -261,6 +262,7 @@ msgtype,channeld_add_inflight,7216 msgdata,channeld_add_inflight,remote_funding,pubkey, msgdata,channeld_add_inflight,tx_id,bitcoin_txid, msgdata,channeld_add_inflight,tx_outnum,u32, +msgdata,channeld_add_inflight,funding_tx_index,u32, msgdata,channeld_add_inflight,feerate,u32, msgdata,channeld_add_inflight,satoshis,amount_sat, msgdata,channeld_add_inflight,splice_amount,s64, diff --git a/channeld/full_channel.c b/channeld/full_channel.c index 5e6586250caf..0244714d67b4 100644 --- a/channeld/full_channel.c +++ b/channeld/full_channel.c @@ -74,6 +74,7 @@ static bool balance_ok(const struct balance *balance, struct channel *new_full_channel(const tal_t *ctx, const struct channel_id *cid, const struct bitcoin_outpoint *funding, + u32 funding_tx_index, u32 minimum_depth, const struct height_states *blockheight_states, u32 lease_expiry, @@ -93,6 +94,7 @@ struct channel *new_full_channel(const tal_t *ctx, struct channel *channel = new_initial_channel(ctx, cid, funding, + funding_tx_index, minimum_depth, blockheight_states, lease_expiry, diff --git a/channeld/full_channel.h b/channeld/full_channel.h index d1cb84c3ffe3..47d09f5aeeca 100644 --- a/channeld/full_channel.h +++ b/channeld/full_channel.h @@ -36,6 +36,7 @@ struct existing_htlc; struct channel *new_full_channel(const tal_t *ctx, const struct channel_id *cid, const struct bitcoin_outpoint *funding, + u32 funding_tx_index, u32 minimum_depth, const struct height_states *blockheight_states, u32 lease_expiry, diff --git a/channeld/inflight.c b/channeld/inflight.c index 475e6698fdec..838ebd2e6c89 100644 --- a/channeld/inflight.c +++ b/channeld/inflight.c @@ -9,6 +9,7 @@ struct inflight *fromwire_inflight(const tal_t *ctx, const u8 **cursor, size_t * struct inflight *inflight = tal(ctx, struct inflight); fromwire_bitcoin_outpoint(cursor, max, &inflight->outpoint); + inflight->funding_tx_index = fromwire_u32(cursor, max); fromwire_pubkey(cursor, max, &inflight->remote_funding); inflight->amnt = fromwire_amount_sat(cursor, max); inflight->remote_tx_sigs = fromwire_bool(cursor, max); @@ -41,6 +42,7 @@ struct inflight *fromwire_inflight(const tal_t *ctx, const u8 **cursor, size_t * void towire_inflight(u8 **pptr, const struct inflight *inflight) { towire_bitcoin_outpoint(pptr, &inflight->outpoint); + towire_u32(pptr, inflight->funding_tx_index); towire_pubkey(pptr, &inflight->remote_funding); towire_amount_sat(pptr, inflight->amnt); towire_bool(pptr, inflight->remote_tx_sigs); diff --git a/channeld/inflight.h b/channeld/inflight.h index 025dc450240d..b34fe785f00a 100644 --- a/channeld/inflight.h +++ b/channeld/inflight.h @@ -10,6 +10,9 @@ struct inflight { /* The new channel outpoint */ struct bitcoin_outpoint outpoint; + /* Which funding tx this is: 0 for the original funding (incl. RBF + * attempts), incrementing by 1 for each splice. */ + u32 funding_tx_index; struct pubkey remote_funding; struct amount_sat amnt; bool remote_tx_sigs; diff --git a/channeld/test/run-full_channel.c b/channeld/test/run-full_channel.c index 1f8b10ba92a5..7446c7ad1863 100644 --- a/channeld/test/run-full_channel.c +++ b/channeld/test/run-full_channel.c @@ -492,7 +492,7 @@ int main(int argc, const char *argv[]) feerate_per_kw[LOCAL] = feerate_per_kw[REMOTE] = 15000; derive_channel_id(&cid, &funding); lchannel = new_full_channel(tmpctx, &cid, - &funding, 0, + &funding, 0, 0, take(new_height_states(NULL, LOCAL, &blockheight)), 0, /* No channel lease */ funding_amount, to_local, @@ -505,7 +505,7 @@ int main(int argc, const char *argv[]) &remote_funding_pubkey, take(channel_type_static_remotekey(NULL)), false, LOCAL); rchannel = new_full_channel(tmpctx, &cid, - &funding, 0, + &funding, 0, 0, take(new_height_states(NULL, REMOTE, &blockheight)), 0, /* No channel lease */ funding_amount, to_remote, diff --git a/common/initial_channel.c b/common/initial_channel.c index 417b766cc861..ffcb9a771069 100644 --- a/common/initial_channel.c +++ b/common/initial_channel.c @@ -12,6 +12,7 @@ struct channel *new_initial_channel(const tal_t *ctx, const struct channel_id *cid, const struct bitcoin_outpoint *funding, + u32 funding_tx_index, u32 minimum_depth, const struct height_states *height_states TAKES, u32 lease_expiry, @@ -46,6 +47,7 @@ struct channel *new_initial_channel(const tal_t *ctx, channel->cid = *cid; channel->funding = *funding; + channel->funding_tx_index = funding_tx_index; channel->funding_sats = funding_sats; channel->minimum_depth = minimum_depth; channel->lease_expiry = lease_expiry; @@ -156,6 +158,7 @@ struct bitcoin_tx *initial_channel_tx(const tal_t *ctx, const char *channel_update_funding(struct channel *channel, const struct bitcoin_outpoint *funding, + u32 funding_tx_index, struct amount_sat funding_sats, s64 splice_amnt) { @@ -164,6 +167,7 @@ const char *channel_update_funding(struct channel *channel, channel->funding = *funding; channel->funding_sats = funding_sats; + channel->funding_tx_index = funding_tx_index; if (splice_amnt * 1000 + channel->view[LOCAL].owed[LOCAL].millisatoshis < 0) /* Raw: splicing */ return tal_fmt(tmpctx, "Channel funding update would make local" diff --git a/common/initial_channel.h b/common/initial_channel.h index 5204efe22945..f3c5312c7d7b 100644 --- a/common/initial_channel.h +++ b/common/initial_channel.h @@ -39,6 +39,10 @@ struct channel { /* Funding txid and output. */ struct bitcoin_outpoint funding; + /* Which funding tx this is: 0 for the original funding (incl. RBF + * attempts), incrementing by 1 for each splice. */ + u32 funding_tx_index; + /* Keys used to spend funding tx. */ struct pubkey funding_pubkey[NUM_SIDES]; @@ -88,6 +92,7 @@ struct channel { * @ctx: tal context to allocate return value from. * @cid: The channel's id. * @funding: The commitment transaction id/outnum + * @funding_tx_index: 0 for the original funding, +1 for each splice. * @minimum_depth: The minimum confirmations needed for funding transaction. * @height_states: The blockheight update states. * @lease_expiry: Block the lease expires. @@ -109,6 +114,7 @@ struct channel { struct channel *new_initial_channel(const tal_t *ctx, const struct channel_id *cid, const struct bitcoin_outpoint *funding, + u32 funding_tx_index, u32 minimum_depth, const struct height_states *height_states TAKES, u32 lease_expiry, @@ -154,6 +160,7 @@ struct bitcoin_tx *initial_channel_tx(const tal_t *ctx, */ const char *channel_update_funding(struct channel *channel, const struct bitcoin_outpoint *funding, + u32 funding_tx_index, struct amount_sat funding_sats, s64 splice_amnt); diff --git a/devtools/mkcommit.c b/devtools/mkcommit.c index 48494ac79819..dfa558cc89d9 100644 --- a/devtools/mkcommit.c +++ b/devtools/mkcommit.c @@ -399,7 +399,7 @@ int main(int argc, char *argv[]) channel = new_full_channel(NULL, &cid, - &funding, 1, + &funding, 0, 1, take(new_height_states(NULL, fee_payer, &blockheight)), 0, /* Defaults to no lease */ diff --git a/lightningd/channel.c b/lightningd/channel.c index 692d81980150..05820b55c1d5 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -171,6 +171,7 @@ struct channel_inflight * new_inflight(struct channel *channel, struct pubkey *remote_funding, const struct bitcoin_outpoint *funding_outpoint, + u32 funding_tx_index, u32 funding_feerate, struct amount_sat total_funds, struct amount_sat our_funds, @@ -199,6 +200,7 @@ new_inflight(struct channel *channel, funding->splice_remote_funding = tal_steal(funding, remote_funding); inflight->funding = funding; + inflight->funding_tx_index = funding_tx_index; inflight->channel = channel; inflight->remote_tx_sigs = false; inflight->funding_psbt = tal_steal(inflight, psbt); @@ -506,6 +508,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid, u64 next_index_remote, u64 next_htlc_id, const struct bitcoin_outpoint *funding, + u32 funding_tx_index, struct amount_sat funding_sats, struct amount_msat push, struct amount_sat our_funds, @@ -612,6 +615,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid, channel->next_index[REMOTE] = next_index_remote; channel->next_htlc_id = next_htlc_id; channel->funding = *funding; + channel->funding_tx_index = funding_tx_index; channel->funding_sats = funding_sats; channel->funding_spend_watch = NULL; channel->push = push; @@ -1350,4 +1354,3 @@ const u8 *channel_update_for_error(const tal_t *ctx, return channel_gossip_update_for_error(ctx, channel); } - diff --git a/lightningd/channel.h b/lightningd/channel.h index 8e3a499ad09c..63345afa3867 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -58,6 +58,9 @@ struct channel_inflight { /* Funding info */ const struct funding_info *funding; + /* Which funding tx this is: 0 for the original funding (incl. RBF + * attempts), incrementing by 1 for each splice. */ + u32 funding_tx_index; struct wally_psbt *funding_psbt; bool remote_tx_sigs; bool tx_broadcast; @@ -203,6 +206,9 @@ struct channel { /* Funding outpoint and amount */ struct bitcoin_outpoint funding; + /* Which funding tx this is: 0 for the original funding (incl. RBF + * attempts), incrementing by 1 for each splice. */ + u32 funding_tx_index; struct amount_sat funding_sats; /* Watch we have on funding output. */ @@ -394,6 +400,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid, u64 next_index_remote, u64 next_htlc_id, const struct bitcoin_outpoint *funding, + u32 funding_tx_index, struct amount_sat funding_sats, struct amount_msat push, struct amount_sat our_funds, @@ -458,6 +465,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid, struct channel_inflight *new_inflight(struct channel *channel, struct pubkey *remote_funding STEALS, const struct bitcoin_outpoint *funding_outpoint, + u32 funding_tx_index, u32 funding_feerate, struct amount_sat funding_sat, struct amount_sat our_funds, diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index 202f0103990e..7c35d9f3706d 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -894,6 +894,7 @@ static void handle_add_inflight(struct lightningd *ld, { struct pubkey *remote_funding = tal(tmpctx, struct pubkey); struct bitcoin_outpoint outpoint; + u32 funding_tx_index; u32 feerate; struct amount_sat satoshis; s64 splice_amnt; @@ -906,6 +907,7 @@ static void handle_add_inflight(struct lightningd *ld, remote_funding, &outpoint.txid, &outpoint.n, + &funding_tx_index, &feerate, &satoshis, &splice_amnt, @@ -922,6 +924,7 @@ static void handle_add_inflight(struct lightningd *ld, inflight = new_inflight(channel, remote_funding, &outpoint, + funding_tx_index, feerate, satoshis, channel->our_funds, @@ -1902,6 +1905,7 @@ bool peer_start_channeld(struct channel *channel, infcopy->remote_funding = *inflight->funding->splice_remote_funding; infcopy->outpoint = inflight->funding->outpoint; + infcopy->funding_tx_index = inflight->funding_tx_index; infcopy->amnt = inflight->funding->total_funds; infcopy->remote_tx_sigs = inflight->remote_tx_sigs; infcopy->splice_amnt = inflight->funding->splice_amnt; @@ -1929,6 +1933,7 @@ bool peer_start_channeld(struct channel *channel, &channel->cid, &channel->funding, channel->funding_sats, + channel->funding_tx_index, channel->minimum_depth, curr_blockheight, channel->blockheight_states, diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index 7c440e59098b..3fac4050eb5f 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -1274,6 +1274,7 @@ wallet_update_channel(struct lightningd *ld, inflight = new_inflight(channel, NULL, &channel->funding, + 0, funding_feerate, channel->funding_sats, channel->our_funds, @@ -1500,6 +1501,7 @@ wallet_commit_channel(struct lightningd *ld, inflight = new_inflight(channel, NULL, &channel->funding, + 0, funding_feerate, channel->funding_sats, channel->our_funds, diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index 6cc3d82f1e61..5ab58b245b17 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -182,6 +182,7 @@ wallet_commit_channel(struct lightningd *ld, uc->minimum_depth, 1, 1, 0, funding, + 0, funding_sats, push, local_funding, @@ -1627,6 +1628,7 @@ static struct channel *stub_chan(struct command *cmd, 0, 1, 1, 1, &funding, + 0, funding_sats, AMOUNT_MSAT(0), AMOUNT_SAT(0), diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index bfee7c084e6e..b98b24b301ea 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -2297,6 +2297,7 @@ void update_channel_from_inflight(struct lightningd *ld, bool is_splice) { channel->funding = inflight->funding->outpoint; + channel->funding_tx_index = inflight->funding_tx_index; channel->funding_sats = inflight->funding->total_funds; channel->our_funds = inflight->funding->our_funds; diff --git a/openingd/dualopend.c b/openingd/dualopend.c index e1940a44e516..b13c4848912d 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -2130,6 +2130,7 @@ static void revert_channel_state(struct state *state) state->channel = new_initial_channel(state, &state->channel_id, &tx_state->funding, + 0, state->minimum_depth, take(new_height_states(NULL, opener, &tx_state->blockheight)), @@ -2204,6 +2205,7 @@ static u8 *accepter_commits(struct state *state, state->channel = new_initial_channel(state, &state->channel_id, &tx_state->funding, + 0, state->minimum_depth, take(new_height_states(NULL, REMOTE, &tx_state->blockheight)), @@ -2849,6 +2851,7 @@ static u8 *opener_commits(struct state *state, state->channel = new_initial_channel(state, &state->channel_id, &tx_state->funding, + 0, state->minimum_depth, take(new_height_states(NULL, LOCAL, &state->tx_state->blockheight)), @@ -4456,6 +4459,7 @@ int main(int argc, char *argv[]) state->channel = new_initial_channel(state, &state->channel_id, &state->tx_state->funding, + 0, state->minimum_depth, take(new_height_states(NULL, opener, &state->tx_state->blockheight)), diff --git a/openingd/openingd.c b/openingd/openingd.c index 52e50572e2e0..60e1564d443c 100644 --- a/openingd/openingd.c +++ b/openingd/openingd.c @@ -590,6 +590,7 @@ static bool funder_finalize_channel_setup(struct state *state, state->channel = new_initial_channel(state, &cid, &state->funding, + 0, state->minimum_depth, NULL, 0, /* No channel lease */ state->funding_sats, @@ -1156,6 +1157,7 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg) state->channel = new_initial_channel(state, &state->channel_id, &state->funding, + 0, state->minimum_depth, NULL, 0, /* No channel lease */ state->funding_sats, diff --git a/tests/fuzz/fuzz-full_channel.c b/tests/fuzz/fuzz-full_channel.c index 977b7367b697..4930ad9c2d3f 100644 --- a/tests/fuzz/fuzz-full_channel.c +++ b/tests/fuzz/fuzz-full_channel.c @@ -208,7 +208,7 @@ static void init_channels(const tal_t *ctx, const u8 **cursor, size_t *max, derive_channel_id(&cid, funding); - *lchannel = new_full_channel(ctx, &cid, funding, 0, + *lchannel = new_full_channel(ctx, &cid, funding, 0, 0, take(new_height_states(NULL, LOCAL, blockheight)), 0, *funding_amount, to_local, take(new_fee_states(NULL, LOCAL, &feerate_per_kw[LOCAL])), @@ -216,7 +216,7 @@ static void init_channels(const tal_t *ctx, const u8 **cursor, size_t *max, &localbase, &remotebase, &local_funding_pubkey, &remote_funding_pubkey, take(channel_type_static_remotekey(NULL)), false, LOCAL); - *rchannel = new_full_channel(ctx, &cid, funding, 0, + *rchannel = new_full_channel(ctx, &cid, funding, 0, 0, take(new_height_states(NULL, REMOTE, blockheight)), 0, *funding_amount, to_remote, take(new_fee_states(NULL, REMOTE, &feerate_per_kw[REMOTE])), diff --git a/tests/fuzz/fuzz-initial_channel.c b/tests/fuzz/fuzz-initial_channel.c index 52eca85fa3f5..e21b7151ec95 100644 --- a/tests/fuzz/fuzz-initial_channel.c +++ b/tests/fuzz/fuzz-initial_channel.c @@ -46,7 +46,7 @@ static void test_channel_update_funding(struct channel *channel, const u8 **curs funding_sats.satoshis %= MAX_SATS; /* Raw: fuzzing */ splice_amnt = fromwire_s64(cursor, max) % MAX_SATS; - channel_update_funding(channel, &funding, funding_sats, splice_amnt); + channel_update_funding(channel, &funding, 0, funding_sats, splice_amnt); } void run(const uint8_t *data, size_t size) @@ -98,6 +98,7 @@ void run(const uint8_t *data, size_t size) for (enum side opener = 0; opener < NUM_SIDES; opener++) { channel = new_initial_channel(tmpctx, &cid, &funding, + 0, minimum_depth, take(new_height_states(NULL, opener, &blockheight)), diff --git a/tests/plugins/channeld_fakenet.c b/tests/plugins/channeld_fakenet.c index f43384fa371c..131c869e3a3f 100644 --- a/tests/plugins/channeld_fakenet.c +++ b/tests/plugins/channeld_fakenet.c @@ -1052,6 +1052,7 @@ static struct channel *handle_init(struct info *info, const u8 *init_msg) struct ext_key final_ext_key; u8 *fwd_msg; u32 minimum_depth, lease_expiry; + u32 funding_tx_index; struct secret last_remote_per_commit_secret; struct penalty_base *pbases; struct channel_type *channel_type; @@ -1089,6 +1090,7 @@ static struct channel *handle_init(struct info *info, const u8 *init_msg) &channel_id, &funding, &funding_sats, + &funding_tx_index, &minimum_depth, &info->current_block_height, &info->blockheight_states, @@ -1141,6 +1143,7 @@ static struct channel *handle_init(struct info *info, const u8 *init_msg) status_debug("Parsed init..."); channel = new_full_channel(info, &channel_id, &funding, + funding_tx_index, minimum_depth, info->blockheight_states, lease_expiry, diff --git a/tests/test_splicing_disconnect.py b/tests/test_splicing_disconnect.py index 5a2ffb8a99ea..8e8a0fef1da9 100644 --- a/tests/test_splicing_disconnect.py +++ b/tests/test_splicing_disconnect.py @@ -69,7 +69,7 @@ def test_splice_disconnect_sig(node_factory, bitcoind): @pytest.mark.openchannel('v1') @pytest.mark.openchannel('v2') @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') -@pytest.mark.xfail(strict=True, reason="funding_tx_index tracking not yet implemented") +@pytest.mark.xfail(strict=True, reason="channel_ready wrongly retransmitted after splice until funding_tx_index detection") def test_splice_reconnect_after_lock_no_channel_ready(node_factory, bitcoind): # Once a splice locks, channel funding txid is updated to the splice txid. # On reconnect we must still recognise the peer's `my_current_funding_locked` @@ -170,7 +170,6 @@ def test_splice_disconnect_commit(node_factory, bitcoind, executor): @pytest.mark.openchannel('v1') @pytest.mark.openchannel('v2') @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') -@pytest.mark.xfail(strict=True, reason="funding_tx_index tracking not yet implemented") def test_splice_funding_tx_index_increments(node_factory, bitcoind): # funding_tx_index is 0 for the original funding and increments by 1 per # splice. Two sequential splices must reach index 2 on the persisted @@ -202,7 +201,6 @@ def channel_funding_tx_index(): @pytest.mark.openchannel('v1') @pytest.mark.openchannel('v2') @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') -@pytest.mark.xfail(strict=True, reason="funding_tx_index tracking not yet implemented") def test_splice_inflight_funding_tx_index(node_factory, bitcoind): # A pending (not-yet-locked) splice inflight carries funding_tx_index == 1 # (the open was index 0), and the value must survive a restart so the diff --git a/wallet/migrations.c b/wallet/migrations.c index 16b663a64890..28d775334cda 100644 --- a/wallet/migrations.c +++ b/wallet/migrations.c @@ -1182,6 +1182,12 @@ static const struct db_migration dbmigrations[] = { * writes stop in the release that removes chaintopology, freezing all * the legacy tables at the same height. */ {NULL, migrate_backfill_bwatch_tables, NULL, NULL}, + + /* Add fields for funding transaction indices to channel_funding_inflights and channels tables. + * This is 0 for the original funding transaction, and is incremented for each splice transaction. + * This allows us to track the funding transaction index for each channel, which is important for splicing and other operations. */ + {SQL("ALTER TABLE channel_funding_inflights ADD funding_tx_index INTEGER DEFAULT 0"), NULL}, + {SQL("ALTER TABLE channels ADD funding_tx_index INTEGER DEFAULT 0"), NULL}, }; const struct db_migration *get_db_migrations(size_t *num) diff --git a/wallet/test/run-chain_moves_duplicate-detect.c b/wallet/test/run-chain_moves_duplicate-detect.c index cc2797950bd3..e8c578f12e1d 100644 --- a/wallet/test/run-chain_moves_duplicate-detect.c +++ b/wallet/test/run-chain_moves_duplicate-detect.c @@ -189,6 +189,7 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED, u64 next_index_remote UNNEEDED, u64 next_htlc_id UNNEEDED, const struct bitcoin_outpoint *funding UNNEEDED, + u32 funding_tx_index UNNEEDED, struct amount_sat funding_sats UNNEEDED, struct amount_msat push UNNEEDED, struct amount_sat our_funds UNNEEDED, @@ -261,6 +262,7 @@ struct channel_state_change *new_channel_state_change(const tal_t *ctx UNNEEDED, struct channel_inflight *new_inflight(struct channel *channel UNNEEDED, struct pubkey *remote_funding STEALS UNNEEDED, const struct bitcoin_outpoint *funding_outpoint UNNEEDED, + u32 funding_tx_index UNNEEDED, u32 funding_feerate UNNEEDED, struct amount_sat funding_sat UNNEEDED, struct amount_sat our_funds UNNEEDED, diff --git a/wallet/test/run-db.c b/wallet/test/run-db.c index 801839c678bb..315561c0ead8 100644 --- a/wallet/test/run-db.c +++ b/wallet/test/run-db.c @@ -197,6 +197,7 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED, u64 next_index_remote UNNEEDED, u64 next_htlc_id UNNEEDED, const struct bitcoin_outpoint *funding UNNEEDED, + u32 funding_tx_index UNNEEDED, struct amount_sat funding_sats UNNEEDED, struct amount_msat push UNNEEDED, struct amount_sat our_funds UNNEEDED, @@ -269,6 +270,7 @@ struct channel_state_change *new_channel_state_change(const tal_t *ctx UNNEEDED, struct channel_inflight *new_inflight(struct channel *channel UNNEEDED, struct pubkey *remote_funding STEALS UNNEEDED, const struct bitcoin_outpoint *funding_outpoint UNNEEDED, + u32 funding_tx_index UNNEEDED, u32 funding_feerate UNNEEDED, struct amount_sat funding_sat UNNEEDED, struct amount_sat our_funds UNNEEDED, diff --git a/wallet/test/run-migrate_remove_chain_moves_duplicates.c b/wallet/test/run-migrate_remove_chain_moves_duplicates.c index ad0550a6be5f..dc3804a66ac8 100644 --- a/wallet/test/run-migrate_remove_chain_moves_duplicates.c +++ b/wallet/test/run-migrate_remove_chain_moves_duplicates.c @@ -230,6 +230,7 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED, u64 next_index_remote UNNEEDED, u64 next_htlc_id UNNEEDED, const struct bitcoin_outpoint *funding UNNEEDED, + u32 funding_tx_index UNNEEDED, struct amount_sat funding_sats UNNEEDED, struct amount_msat push UNNEEDED, struct amount_sat our_funds UNNEEDED, @@ -302,6 +303,7 @@ struct channel_state_change *new_channel_state_change(const tal_t *ctx UNNEEDED, struct channel_inflight *new_inflight(struct channel *channel UNNEEDED, struct pubkey *remote_funding STEALS UNNEEDED, const struct bitcoin_outpoint *funding_outpoint UNNEEDED, + u32 funding_tx_index UNNEEDED, u32 funding_feerate UNNEEDED, struct amount_sat funding_sat UNNEEDED, struct amount_sat our_funds UNNEEDED, diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index c44307710943..8cfbd9faf2c6 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -1705,6 +1705,7 @@ static bool test_channel_inflight_crud(struct lightningd *ld, const tal_t *ctx, 8, false, false, &our_config, 101, 1, 1, 1, &outpoint, + 0, funding_sats, AMOUNT_MSAT(0), our_sats, 0, NULL, @@ -1755,7 +1756,7 @@ static bool test_channel_inflight_crud(struct lightningd *ld, const tal_t *ctx, memset(&outpoint, 1, sizeof(outpoint)); mempat(&sig.s, sizeof(sig.s)); - inflight = new_inflight(chan, NULL, &outpoint, 253, + inflight = new_inflight(chan, NULL, &outpoint, 0, 253, funding_sats, our_sats, funding_psbt, @@ -1791,7 +1792,7 @@ static bool test_channel_inflight_crud(struct lightningd *ld, const tal_t *ctx, our_sats = AMOUNT_SAT(555555); memset(&outpoint, 2, sizeof(outpoint)); mempat(&sig.s, sizeof(sig.s)); - inflight = new_inflight(chan, NULL, &outpoint, 300, + inflight = new_inflight(chan, NULL, &outpoint, 0, 300, funding_sats, our_sats, funding_psbt, diff --git a/wallet/wallet.c b/wallet/wallet.c index 5d54491d57be..0b4aaa4c77a7 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -1478,6 +1478,7 @@ void wallet_inflight_add(struct wallet *w, struct channel_inflight *inflight) " channel_id" ", funding_tx_id" ", funding_tx_outnum" + ", funding_tx_index" ", funding_feerate" ", funding_satoshi" ", our_funding_satoshi" @@ -1499,11 +1500,12 @@ void wallet_inflight_add(struct wallet *w, struct channel_inflight *inflight) ", locked_scid" ", i_sent_sigs" ") VALUES (" - "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);")); + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);")); db_bind_u64(stmt, inflight->channel->dbid); db_bind_txid(stmt, &inflight->funding->outpoint.txid); db_bind_int(stmt, inflight->funding->outpoint.n); + db_bind_int(stmt, inflight->funding_tx_index); db_bind_int(stmt, inflight->funding->feerate); db_bind_amount_sat(stmt, inflight->funding->total_funds); db_bind_amount_sat(stmt, inflight->funding->our_funds); @@ -1698,6 +1700,7 @@ wallet_stmt2inflight(struct wallet *w, struct db_stmt *stmt, i_sent_sigs = db_col_int(stmt, "i_sent_sigs"); inflight = new_inflight(chan, remote_funding, &funding, + db_col_int(stmt, "funding_tx_index"), db_col_int(stmt, "funding_feerate"), funding_sat, our_funding_sat, @@ -1749,6 +1752,7 @@ static bool wallet_channel_load_inflights(struct wallet *w, stmt = db_prepare_v2(w->db, SQL("SELECT" " funding_tx_id" ", funding_tx_outnum" + ", funding_tx_index" ", funding_feerate" ", funding_satoshi" ", our_funding_satoshi" @@ -2116,6 +2120,7 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm db_col_u64(stmt, "next_index_remote"), db_col_u64(stmt, "next_htlc_id"), &funding, + db_col_int(stmt, "funding_tx_index"), funding_sat, push_msat, our_funding_sat, @@ -2363,6 +2368,7 @@ static bool wallet_channels_load_active(struct wallet *w) ", next_htlc_id" ", funding_tx_id" ", funding_tx_outnum" + ", funding_tx_index" ", funding_satoshi" ", our_funding_satoshi" ", funding_locked_remote" @@ -2664,6 +2670,7 @@ void wallet_channel_save(struct wallet *w, struct channel *chan) " next_htlc_id=?," " funding_tx_id=?," " funding_tx_outnum=?," + " funding_tx_index=?," " funding_satoshi=?," " our_funding_satoshi=?," " funding_locked_remote=?," @@ -2729,6 +2736,7 @@ void wallet_channel_save(struct wallet *w, struct channel *chan) db_bind_sha256d(stmt, &chan->funding.txid.shad); db_bind_int(stmt, chan->funding.n); + db_bind_int(stmt, chan->funding_tx_index); db_bind_amount_sat(stmt, chan->funding_sats); db_bind_amount_sat(stmt, chan->our_funds); db_bind_int(stmt, chan->remote_channel_ready); From 7043a727929317cb5216cb84baacd90a9ac51384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Wed, 24 Jun 2026 18:40:12 -0300 Subject: [PATCH 3/6] channeld: detect splice on reestablish via funding_tx_index, not txid is_splice_active compared the peer's my_current_funding_locked txid against peer->channel->funding.txid, which is wrong: once a splice locks, funding.txid becomes the splice txid, both sides agree on it, the check falls through, and channel_ready is incorrectly retransmitted on reconnect. Resolve the txid to its funding_tx_index (the current channel funding or a pending splice inflight) and treat > 0 as a splice. Changelog-Fixed: channeld: correctly detect splice transactions when deciding whether to retransmit `channel_ready` on reconnect. --- channeld/channeld.c | 40 +++++++++++++++++++++---------- tests/test_splicing_disconnect.py | 1 - 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index b523463b2570..bdced5f060a7 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -5650,6 +5650,23 @@ static bool capture_premature_msg(const u8 ***shit_lnd_says, const u8 *msg) return true; } +/* Returns the funding_tx_index of the funding tx with this txid: the current + * channel funding, or a pending splice inflight. Returns 0 (the original + * funding) if unknown. + */ +static u32 funding_tx_index_for_txid(const struct peer *peer, + const struct bitcoin_txid *txid) +{ + if (bitcoin_txid_eq(txid, &peer->channel->funding.txid)) + return peer->channel->funding_tx_index; + for (size_t i = 0; i < tal_count(peer->splice_state->inflights); i++) { + const struct inflight *inf = peer->splice_state->inflights[i]; + if (bitcoin_txid_eq(txid, &inf->outpoint.txid)) + return inf->funding_tx_index; + } + return 0; +} + static void peer_reconnect(struct peer *peer, const struct secret *last_remote_per_commit_secret) { @@ -5972,17 +5989,6 @@ static void peer_reconnect(struct peer *peer, "next_funding_txid not recognized."); } - /* "none of those channel_reestablish messages contain - * my_current_funding_locked or next_funding for a splice transaction" */ - bool is_splice_active = local_next_funding - || peer->splice_state->locked_ready[LOCAL] - || remote_next_funding - || (recv_tlvs - && recv_tlvs->my_current_funding_locked - && !bitcoin_txid_eq( - &recv_tlvs->my_current_funding_locked->my_current_funding_locked_txid, - &peer->channel->funding.txid)); - /* BOLT #2: * * A node: @@ -6001,7 +6007,17 @@ static void peer_reconnect(struct peer *peer, if (peer->channel_ready[LOCAL] && peer->next_index[LOCAL] == 1 && next_commitment_number == 1 - && !is_splice_active) { + /* "none of those channel_reestablish messages contain + * my_current_funding_locked or next_funding for a splice + * transaction": a funding_tx_index of 1 or more means the txid came + * from a splice (0 is the original funding). */ + && !local_next_funding + && !peer->splice_state->locked_ready[LOCAL] + && !remote_next_funding + && !(recv_tlvs + && recv_tlvs->my_current_funding_locked + && funding_tx_index_for_txid(peer, + &recv_tlvs->my_current_funding_locked->my_current_funding_locked_txid) > 0)) { struct tlv_channel_ready_tlvs *tlvs = tlv_channel_ready_tlvs_new(tmpctx); tlvs->short_channel_id = &peer->local_alias; diff --git a/tests/test_splicing_disconnect.py b/tests/test_splicing_disconnect.py index 8e8a0fef1da9..a83f709e57e0 100644 --- a/tests/test_splicing_disconnect.py +++ b/tests/test_splicing_disconnect.py @@ -69,7 +69,6 @@ def test_splice_disconnect_sig(node_factory, bitcoind): @pytest.mark.openchannel('v1') @pytest.mark.openchannel('v2') @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') -@pytest.mark.xfail(strict=True, reason="channel_ready wrongly retransmitted after splice until funding_tx_index detection") def test_splice_reconnect_after_lock_no_channel_ready(node_factory, bitcoind): # Once a splice locks, channel funding txid is updated to the splice txid. # On reconnect we must still recognise the peer's `my_current_funding_locked` From d0c2f850ab0b5e73cfd76e8987418a22536d2f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Wed, 5 Aug 2026 22:51:58 -0300 Subject: [PATCH 4/6] fixup! splice: add tests for funding_tx_index tracking The db transaction commits after the log lines and RPC returns the tests wait on, so poll with wait_for instead of reading the db once. --- tests/test_splicing_disconnect.py | 33 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/tests/test_splicing_disconnect.py b/tests/test_splicing_disconnect.py index a83f709e57e0..6e04b97ceebe 100644 --- a/tests/test_splicing_disconnect.py +++ b/tests/test_splicing_disconnect.py @@ -4,7 +4,7 @@ import time from pyln.testing.utils import EXPERIMENTAL_DUAL_FUND from utils import ( - TEST_NETWORK + TEST_NETWORK, wait_for ) @@ -100,9 +100,11 @@ def test_splice_reconnect_after_lock_no_channel_ready(node_factory, bitcoind): l1.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_REESTABLISH') l2.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_REESTABLISH') - # The locked splice must have been persisted with funding_tx_index == 1... - rows = l1.db_query("SELECT funding_tx_index FROM channels;") - assert max(r['funding_tx_index'] for r in rows) == 1 + # The locked splice must have been persisted with funding_tx_index == 1. + # The db transaction commits after the state-change log we waited for, so + # poll rather than reading once. + wait_for(lambda: max(r['funding_tx_index'] for r in + l1.db_query("SELECT funding_tx_index FROM channels;")) == 1) # Drive a payment so both peers finish reestablish; this guarantees any # erroneous channel_ready retransmit is already logged before we assert it @@ -188,13 +190,16 @@ def channel_funding_tx_index(): rows = l1.db_query("SELECT funding_tx_index FROM channels;") return max(r['funding_tx_index'] for r in rows) + # The db transaction commits after the state-change log do_splice() waits + # for, so poll rather than reading once. + # First splice: 0 -> 1 do_splice(100000) - assert channel_funding_tx_index() == 1 + wait_for(lambda: channel_funding_tx_index() == 1) # Second splice: 1 -> 2 do_splice(50000) - assert channel_funding_tx_index() == 2 + wait_for(lambda: channel_funding_tx_index() == 2) @pytest.mark.openchannel('v1') @@ -211,17 +216,19 @@ def test_splice_inflight_funding_tx_index(node_factory, bitcoind): l1.rpc.splicein(chan_id, "100000") - # The pending splice inflight is index 1. - inflights = l1.db_query("SELECT funding_tx_index FROM" - " channel_funding_inflights;") - assert [r['funding_tx_index'] for r in inflights] == [1] + def inflight_funding_tx_indexes(): + rows = l1.db_query("SELECT funding_tx_index FROM" + " channel_funding_inflights;") + return [r['funding_tx_index'] for r in rows] + + # The pending splice inflight is index 1. The db transaction commits + # after the RPC returns, so poll rather than reading once. + wait_for(lambda: inflight_funding_tx_indexes() == [1]) # Restart l1: the inflight (and its index) must reload from the DB. l1.restart() l1.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_REESTABLISH') - inflights = l1.db_query("SELECT funding_tx_index FROM" - " channel_funding_inflights;") - assert [r['funding_tx_index'] for r in inflights] == [1] + assert inflight_funding_tx_indexes() == [1] # And the splice still completes after the restart. bitcoind.generate_block(6, wait_for_mempool=1) From c4af01989ea8bca2c9b80341885ad7d24155031a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Wed, 5 Aug 2026 22:52:41 -0300 Subject: [PATCH 5/6] fixup! splice: track funding_tx_index on inflights and channel funding Initialise funding_tx_index in new_unsaved_channel: the channel is saved before channel->funding is known, so it must be set at creation. Give both migrations their rollback SQL. --- lightningd/channel.c | 4 ++++ wallet/migrations.c | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lightningd/channel.c b/lightningd/channel.c index 05820b55c1d5..7ee26dede189 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -378,6 +378,10 @@ struct channel *new_unsaved_channel(struct peer *peer, channel->next_index[REMOTE] = 1; channel->next_htlc_id = 0; channel->funding_spend_watch = NULL; + /* Original funding: splices increment this. channel->funding itself + * isn't known until commit, but we save the channel then, so this + * must be set now. */ + channel->funding_tx_index = 0; /* FIXME: remove push when v1 deprecated */ channel->push = AMOUNT_MSAT(0); channel->closing_fee_negotiation_step = 50; diff --git a/wallet/migrations.c b/wallet/migrations.c index 28d775334cda..8bd5b48fe950 100644 --- a/wallet/migrations.c +++ b/wallet/migrations.c @@ -1183,11 +1183,13 @@ static const struct db_migration dbmigrations[] = { * the legacy tables at the same height. */ {NULL, migrate_backfill_bwatch_tables, NULL, NULL}, - /* Add fields for funding transaction indices to channel_funding_inflights and channels tables. - * This is 0 for the original funding transaction, and is incremented for each splice transaction. - * This allows us to track the funding transaction index for each channel, which is important for splicing and other operations. */ - {SQL("ALTER TABLE channel_funding_inflights ADD funding_tx_index INTEGER DEFAULT 0"), NULL}, - {SQL("ALTER TABLE channels ADD funding_tx_index INTEGER DEFAULT 0"), NULL}, + /* Which funding tx this is: 0 for the original funding, incrementing + * by 1 for each splice. Lets us tell a splice from the original + * funding on reestablish, once the channel funding txid has moved. */ + {SQL("ALTER TABLE channel_funding_inflights ADD funding_tx_index INTEGER DEFAULT 0"), NULL, + SQL("ALTER TABLE channel_funding_inflights DROP COLUMN funding_tx_index"), NULL}, + {SQL("ALTER TABLE channels ADD funding_tx_index INTEGER DEFAULT 0"), NULL, + SQL("ALTER TABLE channels DROP COLUMN funding_tx_index"), NULL}, }; const struct db_migration *get_db_migrations(size_t *num) From 63a5bef163e626e56392830e9666be62acbeeef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Wed, 5 Aug 2026 22:53:25 -0300 Subject: [PATCH 6/6] fixup! channeld: detect splice on reestablish via funding_tx_index, not txid Restore the original three-condition check and add the single conjunct the spec requires, applied to both the channel_reestablish we sent and the one we received. check_mutual_splice_locked() clears locked_ready[LOCAL] and rewrites channel->funding.txid to the splice txid in the same call, so both previous proxies for "is this a splice transaction" went stale exactly when a splice completed. Keying on funding_tx_index > 0 answers it for either direction; an unrecognized txid is treated as a splice, matching previous behaviour. --- channeld/channeld.c | 59 ++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index bdced5f060a7..83596b41a1c0 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -12,6 +12,7 @@ */ #include "config.h" #include +#include #include #include #include @@ -5650,21 +5651,49 @@ static bool capture_premature_msg(const u8 ***shit_lnd_says, const u8 *msg) return true; } -/* Returns the funding_tx_index of the funding tx with this txid: the current - * channel funding, or a pending splice inflight. Returns 0 (the original - * funding) if unknown. - */ -static u32 funding_tx_index_for_txid(const struct peer *peer, - const struct bitcoin_txid *txid) +/* Is this txid a splice transaction? funding_tx_index is 0 for the original + * funding (including RBF attempts) and increments by 1 for each splice, so it + * answers this regardless of whether the splice has already become the current + * channel funding. We check the channel funding and any pending splice + * inflight; an unrecognized txid is treated as a splice, which is the + * conservative choice for a reestablish we can't account for. */ +static bool txid_is_splice(const struct peer *peer, + const struct bitcoin_txid *txid) { if (bitcoin_txid_eq(txid, &peer->channel->funding.txid)) - return peer->channel->funding_tx_index; + return peer->channel->funding_tx_index > 0; for (size_t i = 0; i < tal_count(peer->splice_state->inflights); i++) { const struct inflight *inf = peer->splice_state->inflights[i]; if (bitcoin_txid_eq(txid, &inf->outpoint.txid)) - return inf->funding_tx_index; + return inf->funding_tx_index > 0; } - return 0; + return true; +} + +/* "none of those `channel_reestablish` messages contain + * `my_current_funding_locked` or `next_funding` for a splice transaction": + * checks both the message we sent and the one we received. */ +static bool reestablish_mentions_splice(const struct peer *peer, + const struct tlv_channel_reestablish_tlvs *send_tlvs, + const struct tlv_channel_reestablish_tlvs *recv_tlvs) +{ + const struct tlv_channel_reestablish_tlvs *tlvs[] = { + send_tlvs, recv_tlvs + }; + + for (size_t i = 0; i < ARRAY_SIZE(tlvs); i++) { + if (!tlvs[i]) + continue; + /* `next_funding` means an interactive tx construction hasn't + * finished; past `channel_ready` that is always a splice. */ + if (tlvs[i]->next_funding) + return true; + if (tlvs[i]->my_current_funding_locked + && txid_is_splice(peer, + &tlvs[i]->my_current_funding_locked->my_current_funding_locked_txid)) + return true; + } + return false; } static void peer_reconnect(struct peer *peer, @@ -6007,17 +6036,7 @@ static void peer_reconnect(struct peer *peer, if (peer->channel_ready[LOCAL] && peer->next_index[LOCAL] == 1 && next_commitment_number == 1 - /* "none of those channel_reestablish messages contain - * my_current_funding_locked or next_funding for a splice - * transaction": a funding_tx_index of 1 or more means the txid came - * from a splice (0 is the original funding). */ - && !local_next_funding - && !peer->splice_state->locked_ready[LOCAL] - && !remote_next_funding - && !(recv_tlvs - && recv_tlvs->my_current_funding_locked - && funding_tx_index_for_txid(peer, - &recv_tlvs->my_current_funding_locked->my_current_funding_locked_txid) > 0)) { + && !reestablish_mentions_splice(peer, send_tlvs, recv_tlvs)) { struct tlv_channel_ready_tlvs *tlvs = tlv_channel_ready_tlvs_new(tmpctx); tlvs->short_channel_id = &peer->local_alias;