From e4caeaabbc4e893d79b50dc9e79f6b08048f254d Mon Sep 17 00:00:00 2001 From: amackillop Date: Tue, 21 Apr 2026 12:14:50 -0700 Subject: [PATCH] Fix liquidity event cancellation in select loop handle_next_event() both consumed events from the queue via next_event_async() and processed them inline, including spawn_blocking(...).await calls for wallet checks. Because this ran as a polled future inside tokio::select!, any tick timer firing while the handler was suspended at an .await point would cancel the future. The event had already been dequeued, so it was silently lost. Split event receipt from processing: next_event_async() is polled as the select! future (cancellation-safe since it only dequeues on Poll::Ready), and handle_event() runs in the handler block which select! guarantees runs to completion before the next iteration. This was the root cause of JIT channel opens and splices timing out in production. The HTLC would be intercepted, the OpenChannel event consumed from the queue, but a timer tick would cancel processing before create_channel was called. The peer would disconnect after 40s and the HTLC would expire. --- src/lib.rs | 5 ++++- src/liquidity.rs | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ea17ffb66..4c3917099 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -651,6 +651,7 @@ impl Node { // First tick fires immediately; consume it so we don't run at t=0. pending_htlc_interval.tick().await; expiry_check_interval.tick().await; + let lm = liquidity_handler.liquidity_manager(); loop { tokio::select! { _ = stop_liquidity_handler.changed() => { @@ -666,7 +667,9 @@ impl Node { _ = expiry_check_interval.tick() => { liquidity_handler.handle_expired_htlcs().await; } - _ = liquidity_handler.handle_next_event() => {} + event = lm.next_event_async() => { + liquidity_handler.handle_event(event).await; + } } } }); diff --git a/src/liquidity.rs b/src/liquidity.rs index 986cca1bb..83a0e7055 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -519,8 +519,12 @@ where } } - pub(crate) async fn handle_next_event(&self) { - match self.liquidity_manager.next_event_async().await { + /// Handles a single liquidity event. Must be called from a context that + /// guarantees the future runs to completion (e.g. a `select!` handler block), + /// since event processing includes `.await` points that are not + /// cancellation-safe. + pub(crate) async fn handle_event(&self, event: LiquidityEvent) { + match event { LiquidityEvent::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { request_id, counterparty_node_id,