diff --git a/src/ffi/types.rs b/src/ffi/types.rs index cc7298cfa..57350c537 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}; +pub 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..59c4af2c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,7 @@ 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; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; @@ -173,7 +173,10 @@ use types::{ HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, }; -pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, SyncAndAsyncKVStore, UserChannelId}; +pub use types::{ + ChannelDetails, ChannelShutdownState, CustomTlvRecord, PeerDetails, SyncAndAsyncKVStore, + UserChannelId, +}; pub use vss_client; use crate::scoring::setup_background_pathfinding_scores_sync; diff --git a/src/types.rs b/src/types.rs index 381bfbd21..b4b5f5a7b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -16,6 +16,7 @@ use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECH use lightning::chain::chainmonitor; use lightning::impl_writeable_tlv_based; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; +pub use lightning::ln::channel_state::ChannelShutdownState; use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::types::ChannelId; @@ -529,6 +530,10 @@ 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. + /// + /// Returns `None` for `ChannelDetails` serialized on LDK versions prior to 0.0.116. + pub channel_shutdown_state: Option, } impl From for ChannelDetails { @@ -584,6 +589,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..928c4ca88 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 diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 3fde52dc4..aaaff14b4 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -33,7 +33,7 @@ use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, UnifiedPaymentResult, }; -use ldk_node::{Builder, Event, NodeError}; +use ldk_node::{Builder, ChannelShutdownState, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::routing::router::RouteParametersConfig; @@ -2108,24 +2108,26 @@ async fn lsps2_client_trusts_lsp() { service_node.sync_wallets().unwrap(); client_node.sync_wallets().unwrap(); - assert_eq!( - client_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == service_node_id) - .unwrap() - .confirmations, - Some(0) - ); - assert_eq!( - service_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == client_node_id) - .unwrap() - .confirmations, - Some(0) - ); + let client_channel = client_node + .list_channels() + .into_iter() + .find(|c| c.counterparty_node_id == service_node_id) + .unwrap(); + assert_eq!(client_channel.confirmations, Some(0)); + assert!(matches!( + client_channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); + let service_channel = service_node + .list_channels() + .into_iter() + .find(|c| c.counterparty_node_id == client_node_id) + .unwrap(); + assert_eq!(service_channel.confirmations, Some(0)); + assert!(matches!( + service_channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); // Now claim the JIT payment, which should release the funding transaction let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; @@ -2152,24 +2154,26 @@ async fn lsps2_client_trusts_lsp() { generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; service_node.sync_wallets().unwrap(); client_node.sync_wallets().unwrap(); - assert_eq!( - client_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == service_node_id) - .unwrap() - .confirmations, - Some(6) - ); - assert_eq!( - service_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == client_node_id) - .unwrap() - .confirmations, - Some(6) - ); + let client_channel = client_node + .list_channels() + .into_iter() + .find(|c| c.counterparty_node_id == service_node_id) + .unwrap(); + assert_eq!(client_channel.confirmations, Some(6)); + assert!(matches!( + client_channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); + let service_channel = service_node + .list_channels() + .into_iter() + .find(|c| c.counterparty_node_id == client_node_id) + .unwrap(); + assert_eq!(service_channel.confirmations, Some(6)); + assert!(matches!( + service_channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -2280,24 +2284,26 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; service_node.sync_wallets().unwrap(); client_node.sync_wallets().unwrap(); - assert_eq!( - client_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == service_node_id) - .unwrap() - .confirmations, - Some(6) - ); - assert_eq!( - service_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == client_node_id) - .unwrap() - .confirmations, - Some(6) - ); + let client_channel = client_node + .list_channels() + .into_iter() + .find(|c| c.counterparty_node_id == service_node_id) + .unwrap(); + assert_eq!(client_channel.confirmations, Some(6)); + assert!(matches!( + client_channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); + let service_channel = service_node + .list_channels() + .into_iter() + .find(|c| c.counterparty_node_id == client_node_id) + .unwrap(); + assert_eq!(service_channel.confirmations, Some(6)); + assert!(matches!( + service_channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -2672,6 +2678,10 @@ async fn open_channel_with_all_with_anchors() { assert!(channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 500); assert_eq!(channel.counterparty_node_id, node_b.node_id()); assert_eq!(channel.funding_txo.unwrap(), funding_txo); + assert!(matches!( + channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); node_a.stop().unwrap(); node_b.stop().unwrap(); @@ -2723,6 +2733,10 @@ async fn open_channel_with_all_without_anchors() { assert!(channel.channel_value_sats > premine_amount_sat - 500); assert_eq!(channel.counterparty_node_id, node_b.node_id()); assert_eq!(channel.funding_txo.unwrap(), funding_txo); + assert!(matches!( + channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); node_a.stop().unwrap(); node_b.stop().unwrap(); @@ -2766,6 +2780,10 @@ async fn splice_in_with_all_balance() { assert_eq!(channels.len(), 1); assert_eq!(channels[0].channel_value_sats, channel_amount_sat); assert_eq!(channels[0].funding_txo.unwrap(), funding_txo); + assert!(matches!( + channels[0].channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); let balance_before_splice = node_a.list_balances().spendable_onchain_balance_sats; assert!(balance_before_splice > 0); @@ -2787,6 +2805,10 @@ async fn splice_in_with_all_balance() { let channels = node_a.list_channels(); assert_eq!(channels.len(), 1); let channel = &channels[0]; + assert!(matches!( + channel.channel_shutdown_state, + None | Some(ChannelShutdownState::NotShuttingDown) + )); assert!( channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 1000, "Channel value {} should be close to premined amount {} minus anchor reserve {} and fees",