diff --git a/src/ffi/types.rs b/src/ffi/types.rs index a153a77ef5..2e8bb68e49 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -24,7 +24,10 @@ use bitcoin::secp256k1::PublicKey; pub use bitcoin::{Address, BlockHash, Network, OutPoint, ScriptBuf, Txid}; pub use lightning::chain::channelmonitor::BalanceSource; pub use lightning::events::{ClosureReason, PaymentFailureReason}; -use lightning::ln::channel_state::{ChannelShutdownState, CounterpartyForwardingInfo}; +use lightning::ln::channel_state::{ + ChannelShutdownState, CounterpartyForwardingInfo, InboundHTLCDetails, InboundHTLCStateDetails, + OutboundHTLCDetails, OutboundHTLCStateDetails, +}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; pub use lightning::ln::types::ChannelId; @@ -1700,6 +1703,139 @@ pub enum ChannelShutdownState { ShutdownComplete, } +/// Exposes the state of pending inbound HTLCs. +/// +/// This can be used to inspect what next message an HTLC is waiting for to advance its state. +#[uniffi::remote(Enum)] +#[derive(Clone, Debug, PartialEq)] +pub enum InboundHTLCStateDetails { + /// We have added this HTLC in our commitment transaction by receiving commitment_signed and + /// returning revoke_and_ack. We are awaiting the appropriate revoke_and_ack's from the remote + /// before this HTLC is included on the remote commitment transaction. + AwaitingRemoteRevokeToAdd, + /// This HTLC has been included in the commitment_signed and revoke_and_ack messages on both sides + /// and is included in both commitment transactions. + /// + /// This HTLC is now safe to either forward or be claimed as a payment by us. The HTLC will + /// remain in this state until the forwarded upstream HTLC has been resolved and we resolve this + /// HTLC correspondingly, or until we claim it as a payment. If it is part of a multipart + /// payment, it will only be claimed together with other required parts. + Committed, + /// We have received the preimage for this HTLC and it is being removed by fulfilling it with + /// update_fulfill_htlc. This HTLC is still on both commitment transactions, but we are awaiting + /// the appropriate revoke_and_ack's from the remote before this HTLC is removed from the remote + /// commitment transaction after update_fulfill_htlc. + AwaitingRemoteRevokeToRemoveFulfill, + /// The HTLC is being removed by failing it with update_fail_htlc or update_fail_malformed_htlc. + /// This HTLC is still on both commitment transactions, but we are awaiting the appropriate + /// revoke_and_ack's from the remote before this HTLC is removed from the remote commitment + /// transaction. + AwaitingRemoteRevokeToRemoveFail, +} + +/// Exposes details around pending inbound HTLCs. +#[uniffi::remote(Record)] +pub struct InboundHTLCDetails { + /// The HTLC ID. + /// The IDs are incremented by 1 starting from 0 for each offered HTLC. + /// They are unique per channel and inbound/outbound direction, unless an HTLC was only announced + /// and not part of any commitment transaction. + pub htlc_id: u64, + /// The amount in msat. + pub amount_msat: u64, + /// The block height at which this HTLC expires. + pub cltv_expiry: u32, + /// The payment hash. + pub payment_hash: PaymentHash, + /// The state of the HTLC in the state machine. + /// + /// Determines on which commitment transactions the HTLC is included and what message the HTLC is + /// waiting for to advance to the next state. + /// + /// LDK will always fill this field in, but when downgrading to prior versions of LDK, new + /// states may result in `None` here. + pub state: Option, + /// Whether the HTLC has an output below the local dust limit. If so, the output will be trimmed + /// from the local commitment transaction and added to the commitment transaction fee. + /// For non-anchor channels, this takes into account the cost of the second-stage HTLC + /// transactions as well. + /// + /// When the local commitment transaction is broadcasted as part of a unilateral closure, + /// the value of this HTLC will therefore not be claimable but instead burned as a transaction + /// fee. + /// + /// Note that dust limits are specific to each party. An HTLC can be dust for the local + /// commitment transaction but not for the counterparty's commitment transaction and vice versa. + pub is_dust: bool, +} + +/// Exposes the state of pending outbound HTLCs. +/// +/// This can be used to inspect what next message an HTLC is waiting for to advance its state. +#[uniffi::remote(Enum)] +#[derive(Clone, Debug, PartialEq)] +pub enum OutboundHTLCStateDetails { + /// We are awaiting the appropriate revoke_and_ack's from the remote before the HTLC is added + /// on the remote's commitment transaction after update_add_htlc. + AwaitingRemoteRevokeToAdd, + /// The HTLC has been added to the remote's commitment transaction by sending commitment_signed + /// and receiving revoke_and_ack in return. + /// + /// The HTLC will remain in this state until the remote node resolves the HTLC, or until we + /// unilaterally close the channel due to a timeout with an uncooperative remote node. + Committed, + /// The HTLC has been fulfilled successfully by the remote with a preimage in update_fulfill_htlc, + /// and we removed the HTLC from our commitment transaction by receiving commitment_signed and + /// returning revoke_and_ack. We are awaiting the appropriate revoke_and_ack's from the remote + /// for the removal from its commitment transaction. + AwaitingRemoteRevokeToRemoveSuccess, + /// The HTLC has been failed by the remote with update_fail_htlc or update_fail_malformed_htlc, + /// and we removed the HTLC from our commitment transaction by receiving commitment_signed and + /// returning revoke_and_ack. We are awaiting the appropriate revoke_and_ack's from the remote + /// for the removal from its commitment transaction. + AwaitingRemoteRevokeToRemoveFailure, +} + +/// Exposes details around pending outbound HTLCs. +#[uniffi::remote(Record)] +pub struct OutboundHTLCDetails { + /// The HTLC ID. + /// The IDs are incremented by 1 starting from 0 for each offered HTLC. + /// They are unique per channel and inbound/outbound direction, unless an HTLC was only announced + /// and not part of any commitment transaction. + /// + /// Not present when we are awaiting a remote revocation and the HTLC is not added yet. + pub htlc_id: Option, + /// The amount in msat. + pub amount_msat: u64, + /// The block height at which this HTLC expires. + pub cltv_expiry: u32, + /// The payment hash. + pub payment_hash: PaymentHash, + /// The state of the HTLC in the state machine. + /// + /// Determines on which commitment transactions the HTLC is included and what message the HTLC is + /// waiting for to advance to the next state. + /// + /// LDK will always fill this field in, but when downgrading to prior versions of LDK, new + /// states may result in `None` here. + pub state: Option, + /// The extra fee being skimmed off the top of this HTLC. + pub skimmed_fee_msat: Option, + /// Whether the HTLC has an output below the local dust limit. If so, the output will be trimmed + /// from the local commitment transaction and added to the commitment transaction fee. + /// For non-anchor channels, this takes into account the cost of the second-stage HTLC + /// transactions as well. + /// + /// When the local commitment transaction is broadcasted as part of a unilateral closure, + /// the value of this HTLC will therefore not be claimable but instead burned as a transaction + /// fee. + /// + /// Note that dust limits are specific to each party. An HTLC can be dust for the local + /// commitment transaction but not for the counterparty's commitment transaction and vice versa. + pub is_dust: bool, +} + /// 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 6d13141c6a..526e3ec746 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -155,7 +155,10 @@ use lightning::chain::BlockLocator; use lightning::impl_writeable_tlv_based; use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; -pub use lightning::ln::channel_state::ChannelShutdownState; +pub use lightning::ln::channel_state::{ + ChannelShutdownState, InboundHTLCDetails, InboundHTLCStateDetails, OutboundHTLCDetails, + OutboundHTLCStateDetails, +}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::{BaseMessageHandler, SocketAddress}; use lightning::ln::peer_handler::CustomMessageHandler; diff --git a/src/types.rs b/src/types.rs index fd86d1bcd8..25c7870871 100644 --- a/src/types.rs +++ b/src/types.rs @@ -16,6 +16,7 @@ use lightning::chain::chainmonitor; use lightning::impl_writeable_tlv_based; use lightning::ln::channel_state::{ ChannelDetails as LdkChannelDetails, ChannelShutdownState, CounterpartyForwardingInfo, + InboundHTLCDetails, OutboundHTLCDetails, }; use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::IgnoringMessageHandler; @@ -586,6 +587,8 @@ pub struct ChannelDetails { /// an upper-bound. This is intended for use when routing, allowing us to ensure we pick a /// route which is valid. pub next_outbound_htlc_minimum_msat: u64, + /// The maximum value, in satoshis, of the next splice out from our channel balance. + pub next_splice_out_maximum_sat: u64, /// The number of blocks (after our commitment transaction confirms) that we will need to wait /// until we can claim our funds after we force-close the channel. During this time our /// counterparty is allowed to punish us if we broadcasted a stale state. If our counterparty @@ -616,6 +619,21 @@ pub struct ChannelDetails { /// Will be `None` until channel negotiation has completed and the channel type has been /// determined. pub channel_type: Option, + /// Pending inbound HTLCs. + pub pending_inbound_htlcs: Vec, + /// Pending outbound HTLCs. + pub pending_outbound_htlcs: Vec, + /// The current total dust exposure on this channel, in millisatoshis. + /// + /// This is the maximum of the dust exposure on the holder and counterparty commitment + /// transactions. It includes pending HTLCs below the dust threshold and the portion of + /// commitment transaction fees that contributes to dust exposure. + /// + /// This is compared against [`ChannelConfig::max_dust_htlc_exposure`] when determining whether + /// new HTLCs can be accepted or offered on this channel. + /// + /// Will be `None` for objects serialized with LDK versions prior to 0.3. + pub current_dust_exposure_msat: Option, } impl ChannelDetails { @@ -669,6 +687,7 @@ impl ChannelDetails { cltv_expiry_delta: value.config.map(|c| c.cltv_expiry_delta), next_outbound_htlc_limit_msat: value.next_outbound_htlc_limit_msat, next_outbound_htlc_minimum_msat: value.next_outbound_htlc_minimum_msat, + next_splice_out_maximum_sat: value.next_splice_out_maximum_sat, force_close_spend_delay: value.force_close_spend_delay, inbound_htlc_minimum_msat: value .inbound_htlc_minimum_msat @@ -681,6 +700,9 @@ impl ChannelDetails { channel_shutdown_state: value.channel_shutdown_state, reserve_type, channel_type: value.channel_type.map(maybe_wrap), + pending_inbound_htlcs: value.pending_inbound_htlcs, + pending_outbound_htlcs: value.pending_outbound_htlcs, + current_dust_exposure_msat: value.current_dust_exposure_msat, } } } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 83e80c1048..e9fa36ab53 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -460,6 +460,117 @@ async fn channel_full_cycle() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn channel_details_pending_htlcs() { + use ldk_node::{InboundHTLCStateDetails, OutboundHTLCStateDetails}; + + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![ + node_a.onchain_payment().new_address().unwrap(), + node_b.onchain_payment().new_address().unwrap(), + ], + Amount::from_sat(1_000_000), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 500_000, false, &electrsd).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + for node in [&node_a, &node_b] { + let channel = node.list_channels().remove(0); + assert!(channel.pending_inbound_htlcs.is_empty()); + assert!(channel.pending_outbound_htlcs.is_empty()); + assert_eq!(channel.current_dust_exposure_msat, Some(0)); + } + let initial_splice_out_maximum_sat = node_a.list_channels()[0].next_splice_out_maximum_sat; + assert!(initial_splice_out_maximum_sat > 0); + assert!(initial_splice_out_maximum_sat < 500_000); + + // Keep both a dust and a non-dust HTLC pending until we explicitly resolve them. + let amounts_msat = [1_000, 10_000_000]; + let description = + Bolt11InvoiceDescription::Direct(Description::new("pending HTLCs".to_owned()).unwrap()); + let mut payments = Vec::new(); + for (index, amount_msat) in amounts_msat.into_iter().enumerate() { + let preimage = PaymentPreimage([index as u8 + 1; 32]); + let payment_hash = PaymentHash::from(preimage); + let invoice = node_b + .bolt11_payment() + .receive_for_hash(amount_msat, &description.clone().into(), 3600, payment_hash) + .unwrap(); + let outbound_id = node_a.bolt11_payment().send(&invoice, None).unwrap(); + let (inbound_id, _) = expect_payment_claimable_event!(node_b, payment_hash, amount_msat); + payments.push((outbound_id, inbound_id, payment_hash, preimage)); + } + + let outbound_channel = node_a.list_channels().remove(0); + let inbound_channel = node_b.list_channels().remove(0); + assert_eq!(outbound_channel.pending_outbound_htlcs.len(), 2); + assert!(outbound_channel.pending_inbound_htlcs.is_empty()); + assert_eq!(inbound_channel.pending_inbound_htlcs.len(), 2); + assert!(inbound_channel.pending_outbound_htlcs.is_empty()); + assert_eq!(outbound_channel.current_dust_exposure_msat, Some(amounts_msat[0])); + assert_eq!(inbound_channel.current_dust_exposure_msat, Some(amounts_msat[0])); + assert!(outbound_channel.next_splice_out_maximum_sat < initial_splice_out_maximum_sat); + for (index, (_, _, payment_hash, _)) in payments.iter().enumerate() { + let outbound = outbound_channel + .pending_outbound_htlcs + .iter() + .find(|htlc| htlc.payment_hash == *payment_hash) + .unwrap(); + let inbound = inbound_channel + .pending_inbound_htlcs + .iter() + .find(|htlc| htlc.payment_hash == *payment_hash) + .unwrap(); + assert_eq!(outbound.htlc_id, Some(inbound.htlc_id)); + assert_eq!(outbound.amount_msat, amounts_msat[index]); + assert_eq!(inbound.amount_msat, amounts_msat[index]); + assert_eq!(outbound.cltv_expiry, inbound.cltv_expiry); + assert!(inbound.cltv_expiry > node_b.status().current_best_block.height); + assert_eq!(outbound.state, Some(OutboundHTLCStateDetails::Committed)); + assert_eq!(inbound.state, Some(InboundHTLCStateDetails::Committed)); + assert_eq!(outbound.is_dust, index == 0); + assert_eq!(inbound.is_dust, index == 0); + assert_eq!(outbound.skimmed_fee_msat, None); + } + + let (outbound_id, inbound_id, _, preimage) = payments[0]; + node_b.bolt11_payment().claim_for_id(inbound_id, amounts_msat[0], preimage).unwrap(); + expect_payment_received_event!(node_b, amounts_msat[0]); + expect_payment_successful_event!(node_a, outbound_id, None); + node_b.bolt11_payment().fail_for_id(payments[1].1).unwrap(); + expect_event!(node_a, PaymentFailed); + + tokio::time::timeout(Duration::from_secs(common::INTEROP_TIMEOUT_SECS), async { + loop { + let channels = [node_a.list_channels().remove(0), node_b.list_channels().remove(0)]; + if channels.iter().all(|channel| { + channel.pending_inbound_htlcs.is_empty() + && channel.pending_outbound_htlcs.is_empty() + }) { + for channel in channels { + assert_eq!(channel.current_dust_exposure_msat, Some(0)); + } + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("resolved HTLCs should disappear from channel details"); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_force_close() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();