From 3b68c591bf58f9b855a9b87670853b5d6820042d Mon Sep 17 00:00:00 2001 From: Fernando Ledesma Date: Sat, 14 Mar 2026 13:58:43 -0500 Subject: [PATCH] Expose `ChannelDetails::channel_shutdown_state` Add a `ChannelShutdownState` enum mirroring LDK's own type, and expose it as an `Option` field on `ChannelDetails`. --- src/ffi/types.rs | 21 ++++++++++++++ src/lib.rs | 3 +- src/types.rs | 5 +++- tests/common/mod.rs | 68 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/ffi/types.rs b/src/ffi/types.rs index cc7298cfa..9f2be8a23 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -25,6 +25,7 @@ pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Txi pub use lightning::chain::channelmonitor::BalanceSource; use lightning::events::PaidBolt12Invoice as LdkPaidBolt12Invoice; pub use lightning::events::{ClosureReason, PaymentFailureReason}; +use lightning::ln::channel_state::ChannelShutdownState; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; pub use lightning::ln::types::ChannelId; @@ -1408,6 +1409,26 @@ uniffi::custom_type!(LSPSDateTime, String, { }, }); +/// The shutdown state of a channel as returned in [`ChannelDetails::channel_shutdown_state`]. +/// +/// [`ChannelDetails::channel_shutdown_state`]: crate::ChannelDetails::channel_shutdown_state +#[uniffi::remote(Enum)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ChannelShutdownState { + /// Channel has not sent or received a shutdown message. + NotShuttingDown, + /// Local node has sent a shutdown message for this channel. + ShutdownInitiated, + /// Shutdown message exchanges have concluded and the channels are in the midst of + /// resolving all existing open HTLCs before closing can continue. + ResolvingHTLCs, + /// All HTLCs have been resolved, nodes are currently negotiating channel close onchain fee rates. + NegotiatingClosingFee, + /// We've successfully negotiated a closing_signed dance. At this point `ChannelManager` is about + /// to drop the channel. + ShutdownComplete, +} + /// The reason the channel was closed. See individual variants for more details. #[uniffi::remote(Enum)] #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/src/lib.rs b/src/lib.rs index 109ade0ae..2d6d441d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,8 @@ pub use lightning; use lightning::chain::BestBlock; use lightning::impl_writeable_tlv_based; use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; -use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState}; +use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; +pub use lightning::ln::channel_state::ChannelShutdownState; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; diff --git a/src/types.rs b/src/types.rs index 381bfbd21..fa06c637c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -15,7 +15,7 @@ use bitcoin::{OutPoint, ScriptBuf}; use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use lightning::chain::chainmonitor; use lightning::impl_writeable_tlv_based; -use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; +use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState}; use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::types::ChannelId; @@ -529,6 +529,8 @@ pub struct ChannelDetails { pub inbound_htlc_maximum_msat: Option, /// Set of configurable parameters that affect channel operation. pub config: ChannelConfig, + /// The current shutdown state of the channel, if any. + pub channel_shutdown_state: Option, } impl From for ChannelDetails { @@ -584,6 +586,7 @@ impl From for ChannelDetails { inbound_htlc_maximum_msat: value.inbound_htlc_maximum_msat, // unwrap safety: `config` is only `None` for LDK objects serialized prior to 0.0.109. config: value.config.map(|c| c.into()).unwrap(), + channel_shutdown_state: value.channel_shutdown_state, } } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 7854a77f2..595f9affe 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -31,8 +31,8 @@ use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ - Builder, CustomTlvRecord, Event, LightningBalance, Node, NodeError, PendingSweepBalance, - UserChannelId, + Builder, ChannelShutdownState, CustomTlvRecord, Event, LightningBalance, Node, NodeError, + PendingSweepBalance, UserChannelId, }; use lightning::io; use lightning::ln::msgs::SocketAddress; @@ -918,6 +918,28 @@ pub(crate) async fn do_channel_full_cycle( let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); let user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); + // After channel_ready, no shutdown should be in progress on either side. + for channel in node_a.list_channels() { + assert!( + matches!( + channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + ), + "Expected no shutdown in progress on node_a, got {:?}", + channel.channel_shutdown_state, + ); + } + for channel in node_b.list_channels() { + assert!( + matches!( + channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + ), + "Expected no shutdown in progress on node_b, got {:?}", + channel.channel_shutdown_state, + ); + } + println!("\nB receive"); let invoice_amount_1_msat = 2500_000; let invoice_description: Bolt11InvoiceDescription = @@ -1233,6 +1255,20 @@ pub(crate) async fn do_channel_full_cycle( expect_channel_ready_event!(node_a, node_b.node_id()); expect_channel_ready_event!(node_b, node_a.node_id()); + // After the splice-out, the channel must still report no shutdown in progress. + for channel in node_a.list_channels() { + assert!(matches!( + channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); + } + for channel in node_b.list_channels() { + assert!(matches!( + channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); + } + assert_eq!( node_a .list_payments_with_filter(|p| p.direction == PaymentDirection::Inbound @@ -1255,6 +1291,20 @@ pub(crate) async fn do_channel_full_cycle( expect_channel_ready_event!(node_a, node_b.node_id()); expect_channel_ready_event!(node_b, node_a.node_id()); + // After the splice-in, the channel must still report no shutdown in progress. + for channel in node_a.list_channels() { + assert!(matches!( + channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); + } + for channel in node_b.list_channels() { + assert!(matches!( + channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); + } + assert_eq!( node_a .list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound @@ -1269,6 +1319,20 @@ pub(crate) async fn do_channel_full_cycle( node_a.force_close_channel(&user_channel_id_a, node_b.node_id(), None).unwrap(); } else { node_a.close_channel(&user_channel_id_a, node_b.node_id()).unwrap(); + // The cooperative shutdown may complete before we get to check, but if the channel + // is still visible it must already be in a shutdown state. + if let Some(channel) = + node_a.list_channels().into_iter().find(|c| c.user_channel_id == user_channel_id_a) + { + assert!( + !matches!( + channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + ), + "Expected shutdown in progress on node_a, got {:?}", + channel.channel_shutdown_state, + ); + } } expect_event!(node_a, ChannelClosed);