From 218050bc8e70dcd683d18a57ae003ecb9ad6078d Mon Sep 17 00:00:00 2001 From: enaples Date: Tue, 19 May 2026 14:17:30 +0200 Subject: [PATCH 1/2] pytest: test to reproduce failed to retransmit funding transaction `resend_opening_transactions` gated on `channel->depth != 0`, but depth is reset to 0 when channels are loaded from the DB and only repopulated `after begin_topology()` runs. So at startup every channel matched and `bitcoind` rejected the rebroadcast with error `-27` ("Transaction outputs already in utxo set"), facing as an `UNUSUAL` log. Changelog-Fixed: lightningd: don't spuriously try to re-broadcast funding txs of already-confirmed channels on startup. --- tests/test_opening.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_opening.py b/tests/test_opening.py index 918e37bbda44..e474b2c1b32c 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -3018,3 +3018,19 @@ def test_zeroconf_withhold_htlc_failback(node_factory, bitcoind): # l1's channel to l2 is still normal — no force-close assert only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['state'] == 'CHANNELD_NORMAL' + + +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +def test_no_retransmit_confirmed_funding(node_factory): + """An channel must not trigger funding tx re-transmission on restart.""" + l1, _ = node_factory.line_graph(2, wait_for_announce=True) + + # Channel is in CHANNELD_NORMAL and funding tx is confirmed. + assert only_one(l1.rpc.listpeerchannels()['channels'])['state'] == 'CHANNELD_NORMAL' + + l1.restart() + + # Should not have attempted (and failed) to re-broadcast the funding tx. + assert l1.daemon.is_in_log('Failed to re-transmit funding tx') + assert not l1.daemon.is_in_log('Successfully rexmitted funding tx') From 3c2cf6e94024d778b88d0f0ab35d0f601589d95c Mon Sep 17 00:00:00 2001 From: enaples Date: Tue, 19 May 2026 14:19:01 +0200 Subject: [PATCH 2/2] lightningd: don't try to re-xmit funding tx for already-confirmed channels. `resend_opening_transactions` runs at startup before `begin_topology()`, so `channel->depth` is still 0 (its DB-load default) for every channel. The `depth != 0` guard was therefore a no-op, and we issued a `sendrawtransaction` for every committed channel on every restart. For long-confirmed channels bitcoind replies with error `-27` ("Transaction outputs already in utxo set"), surfacing as an `UNUSUAL` log on every startup. Gate on channel state instead: only the three states where the funding or splice tx can still be unconfirmed (`CHANNELD_AWAITING_LOCKIN`, DUALOPEND_AWAITING_LOCKIN`, `CHANNELD_AWAITING_SPLICE`) qualify for rebroadcast. Changelog-Fixed: lightningd: don't spuriously try to re-broadcast funding txs of already-confirmed channels on startup. --- lightningd/peer_control.c | 10 +++++++--- tests/test_opening.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 81af0e8a2e13..6a1cb1aa5473 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -565,12 +565,16 @@ void resend_opening_transactions(struct lightningd *ld) peer = peer_node_id_map_next(ld->peers, &it)) { list_for_each(&peer->channels, channel, list) { struct wally_tx *wtx; - if (channel_state_uncommitted(channel->state)) + /* Only states where the funding/splice tx might + * still be unconfirmed. channel->depth can't be + * used here: it's reset to 0 on DB load and only + * repopulated once topology starts. */ + if (channel->state != CHANNELD_AWAITING_LOCKIN + && channel->state != DUALOPEND_AWAITING_LOCKIN + && channel->state != CHANNELD_AWAITING_SPLICE) continue; if (!channel->funding_psbt || channel->withheld) continue; - if (channel->depth != 0) - continue; wtx = psbt_final_tx(tmpctx, channel->funding_psbt); if (!wtx) continue; diff --git a/tests/test_opening.py b/tests/test_opening.py index e474b2c1b32c..a272e711e85e 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -3032,5 +3032,5 @@ def test_no_retransmit_confirmed_funding(node_factory): l1.restart() # Should not have attempted (and failed) to re-broadcast the funding tx. - assert l1.daemon.is_in_log('Failed to re-transmit funding tx') + assert not l1.daemon.is_in_log('Failed to re-transmit funding tx') assert not l1.daemon.is_in_log('Successfully rexmitted funding tx')