From a6c876bf54683d167afe6e47e606d49c1c00847f Mon Sep 17 00:00:00 2001 From: kimi Date: Fri, 21 Aug 2026 10:35:20 +0930 Subject: [PATCH 1/2] channeld: drain stfu deferred queue on end_stfu_mode While quiescing (STFU), handle_master_request_later queues outgoing HTLC updates (OFFER/FULFILL/FAIL) and fee/block updates on peer->update_queue, but no code path ever dequeued it. Messages were lost permanently when quiescence ended. Fix by moving any pending messages onto from_master when end_stfu_mode runs; the main loop drains from_master via req_in. Changelog-Fixed: channeld: risk of force close if we settle an HTLC during splice STFU. --- channeld/channeld.c | 10 +++++++++ tests/test_connection.py | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/channeld/channeld.c b/channeld/channeld.c index 3848f0d2e837..b883a2d5f1de 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -234,11 +234,21 @@ static bool is_entering_stfu(const struct peer *peer) static void end_stfu_mode(struct peer *peer) { + const u8 *msg; + peer->want_stfu = false; peer->stfu_sent[LOCAL] = peer->stfu_sent[REMOTE] = false; peer->stfu_wait_single_msg = false; peer->on_stfu_success = NULL; + /* Move any pending messages onto from_master; the main + * loop drains that queue via req_in. */ + while ((msg = msg_dequeue(peer->update_queue))) { + status_debug("Requeueing quiescence-deferred %s onto from_master", + channeld_wire_name(fromwire_peektype(msg))); + msg_enqueue(peer->from_master, msg); + } + status_debug("Left STFU mode."); } diff --git a/tests/test_connection.py b/tests/test_connection.py index 99791fd14693..0fa982307f41 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -3811,6 +3811,50 @@ def test_quiescence(node_factory, executor): pass +def test_quiescence_end_delivers_pending_fulfill(node_factory, bitcoind, executor): + """While quiescent, a fulfill queued on stfu_pending_queue must be + delivered when quiescence ends (end_stfu_mode moves it to from_master). + """ + hold_plugin = str(Path(__file__).parent / "plugins" / "hold_htlcs.py") + l1, l2 = node_factory.line_graph(2, fundamount=1000000, + opts={'plugin': [hold_plugin], 'hold-time': 5}) + # l2 needs wallet UTXOs for the splice; l1 opened the channel. + addr = l2.rpc.newaddr('bech32')['bech32'] + bitcoind.rpc.sendtoaddress(addr, 0.002) + bitcoind.generate_block(1, wait_for_mempool=1) + wait_for(lambda: len(l2.rpc.listfunds()['outputs']) == 1) + + # Balance so l2 can send. + inv = l2.rpc.invoice(250000000, 'balance', 'balance') + l1.rpc.xpay(inv['bolt11']) + + inv = l1.rpc.invoice(10000000, 'qend', 'qend') + fut = executor.submit(l2.rpc.xpay, inv['bolt11']) + l1.daemon.wait_for_log('Holding onto an incoming htlc') + + # Start the splice (enters quiescence); the hold plugin delays the + # fulfill past quiescence-entry, so the fulfill lands on + # stfu_pending_queue while l1 is at loop top. + funds = l2.rpc.fundpsbt("111722sat", 0, 0, excess_as_change=True) + r = l2.rpc.splice_init(l2.get_channel_id(l1), 100000, funds['psbt']) + l1.daemon.wait_for_log('STFU complete: we are quiescent') + l2.daemon.wait_for_log('STFU complete: we are quiescent') + + # Hold expires: lightningd resolves the invoice, but the fulfill is + # queued (quiescent). l1 marks the invoice paid. + wait_for(lambda: only_one(l1.rpc.listinvoices('qend')['invoices'])['status'] == 'paid') + + # End quiescence by completing the in-flight splice; end_stfu_mode must + # move the queued fulfill onto from_master and deliver it. + r = l2.rpc.splice_update(l2.get_channel_id(l1), r['psbt']) + r = l2.rpc.splice_update(l2.get_channel_id(l1), r['psbt']) + r = l2.rpc.signpsbt(r['psbt']) + l2.rpc.splice_signed(l2.get_channel_id(l1), r['signed_psbt']) + + # The payment completes: the queued fulfill was delivered. + fut.result(timeout=60) + + def test_htlc_failed_noclose(node_factory): """Test a bug where the htlc timeout would kick in even if the HTLC failed""" l1, l2 = node_factory.line_graph(2) From 954834f3a1d847cc33a7d47884c8808c58af23c0 Mon Sep 17 00:00:00 2001 From: kimi Date: Fri, 21 Aug 2026 10:35:28 +0930 Subject: [PATCH 2/2] channeld: rename update_queue to stfu_pending_queue Better describes its purpose: messages deferred while quiescing (STFU). --- channeld/channeld.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index b883a2d5f1de..17df79676575 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -157,7 +157,7 @@ struct peer { /* After STFU mode is enabled, wait for a single message flag */ bool stfu_wait_single_msg; /* Updates master asked, which we've deferred while quiescing */ - struct msg_queue *update_queue; + struct msg_queue *stfu_pending_queue; /* Callback for when when stfu is negotiated successfully */ void (*on_stfu_success)(struct peer*); @@ -243,7 +243,7 @@ static void end_stfu_mode(struct peer *peer) /* Move any pending messages onto from_master; the main * loop drains that queue via req_in. */ - while ((msg = msg_dequeue(peer->update_queue))) { + while ((msg = msg_dequeue(peer->stfu_pending_queue))) { status_debug("Requeueing quiescence-deferred %s onto from_master", channeld_wire_name(fromwire_peektype(msg))); msg_enqueue(peer->from_master, msg); @@ -377,7 +377,7 @@ static void handle_stfu(struct peer *peer, const u8 *stfu) static bool handle_master_request_later(struct peer *peer, const u8 *msg) { if (is_entering_stfu(peer)) { - msg_enqueue(peer->update_queue, take(msg)); + msg_enqueue(peer->stfu_pending_queue, take(msg)); return true; } return false; @@ -7052,7 +7052,7 @@ int main(int argc, char *argv[]) peer->stfu_sent[LOCAL] = peer->stfu_sent[REMOTE] = false; peer->stfu_wait_single_msg = false; peer->on_stfu_success = NULL; - peer->update_queue = msg_queue_new(peer, false); + peer->stfu_pending_queue = msg_queue_new(peer, false); peer->splice_state = splice_state_new(peer); peer->splicing = NULL;