diff --git a/fuzz/src/router.rs b/fuzz/src/router.rs index c9ceea3d0b3..23ae3e24cda 100644 --- a/fuzz/src/router.rs +++ b/fuzz/src/router.rs @@ -242,6 +242,8 @@ pub fn do_test(data: &[u8], out: Out) { config: None, feerate_sat_per_1000_weight: None, channel_shutdown_state: Some(channelmanager::ChannelShutdownState::NotShuttingDown), + pending_inbound_htlcs: Vec::new(), + pending_outbound_htlcs: Vec::new(), }); } Some(&$first_hops_vec[..]) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index ec4f26664a7..e77026cca7a 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -158,6 +158,104 @@ enum InboundHTLCState { LocalRemoved(InboundHTLCRemovalReason), } +/// Exposes the state of pending inbound HTLCs. +/// +/// At a high level, an HTLC being forwarded from one Lightning node to another Lightning node goes +/// through the following states in the state machine: +/// - Announced for addition by the originating node through the update_add_htlc message. +/// - Added to the commitment transaction of the receiving node and originating node in turn +/// through the exchange of commitment_signed and revoke_and_ack messages. +/// - Announced for resolution (fulfillment or failure) by the receiving node through either one of +/// the update_fulfill_htlc, update_fail_htlc, and update_fail_malformed_htlc messages. +/// - Removed from the commitment transaction of the originating node and receiving node in turn +/// through the exchange of commitment_signed and revoke_and_ack messages. +/// +/// This can be used to inspect what next message an HTLC is waiting for to advance its state. +#[derive(Clone, Debug, PartialEq)] +pub enum InboundHTLCStateDetails { + /// The remote node announced the HTLC with update_add_htlc but the HTLC is not added to any + /// commitment transactions yet. + /// + /// We intend to forward the HTLC as it is correctly formed and is forwardable to the next hop. + RemoteAnnouncedForward, + /// The remote node announced the HTLC with update_add_htlc but the HTLC is not added to any + /// commitment transactions yet. + /// + /// We intend to fail the HTLC as it is malformed or we are unable to forward to the next hop, + /// for example if the peer is disconnected or not enough capacity is available. + RemoteAnnouncedFail, + /// 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. + /// + /// We intend to forward the HTLC as it is correctly formed and is forwardable to the next hop. + AwaitingRemoteRevokeToAddForward, + /// 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. + /// + /// We intend to fail the HTLC as it is malformed or we are unable to forward to the next hop, + /// for example if the peer is disconnected or not enough capacity is available. + AwaitingRemoteRevokeToAddFail, + /// 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 by us. The HTLC will remain in this + /// state until the forwarded upstream HTLC has been resolved, or until it is claimed, + /// potentially together with other HTLCs as part of a multipath payment. + Committed, + /// This HTLC is still on both commitment transactions, but we are awaiting revoke_and_ack from + /// the remote for previous changes before we can announce to remove this HTLC from the remote + /// commitment transaction. + /// + /// We have received the preimage for this HTLC and it will be removed by fulfilling it with + /// update_fulfill_htlc. + AwaitingRemoteRevokeToRemoveFulfill, + /// This HTLC is still on both commitment transactions, but we are awaiting revoke_and_ack from + /// the remote for previous changes before we can announce to remove this HTLC from the remote + /// commitment transaction. + /// + /// The HTLC will be removed by failing it with update_fail_htlc or update_fail_malformed_htlc. + AwaitingRemoteRevokeToRemoveFail, +} + +impl From<&InboundHTLCState> for InboundHTLCStateDetails { + fn from(state: &InboundHTLCState) -> InboundHTLCStateDetails { + match state { + InboundHTLCState::RemoteAnnounced(PendingHTLCStatus::Forward(_)) => + InboundHTLCStateDetails::RemoteAnnouncedForward, + InboundHTLCState::RemoteAnnounced(PendingHTLCStatus::Fail(_)) => + InboundHTLCStateDetails::RemoteAnnouncedFail, + InboundHTLCState::AwaitingRemoteRevokeToAnnounce(PendingHTLCStatus::Forward(_)) => + InboundHTLCStateDetails::AwaitingRemoteRevokeToAddForward, + InboundHTLCState::AwaitingRemoteRevokeToAnnounce(PendingHTLCStatus::Fail(_)) => + InboundHTLCStateDetails::AwaitingRemoteRevokeToAddFail, + InboundHTLCState::AwaitingAnnouncedRemoteRevoke(PendingHTLCStatus::Forward(_)) => + InboundHTLCStateDetails::AwaitingRemoteRevokeToAddForward, + InboundHTLCState::AwaitingAnnouncedRemoteRevoke(PendingHTLCStatus::Fail(_)) => + InboundHTLCStateDetails::AwaitingRemoteRevokeToAddFail, + InboundHTLCState::Committed => + InboundHTLCStateDetails::Committed, + InboundHTLCState::LocalRemoved(InboundHTLCRemovalReason::FailRelay(_)) => + InboundHTLCStateDetails::AwaitingRemoteRevokeToRemoveFail, + InboundHTLCState::LocalRemoved(InboundHTLCRemovalReason::FailMalformed(_)) => + InboundHTLCStateDetails::AwaitingRemoteRevokeToRemoveFail, + InboundHTLCState::LocalRemoved(InboundHTLCRemovalReason::Fulfill(_)) => + InboundHTLCStateDetails::AwaitingRemoteRevokeToRemoveFulfill, + } + } +} + +impl_writeable_tlv_based_enum!(InboundHTLCStateDetails, + (0, RemoteAnnouncedForward) => {}, + (2, RemoteAnnouncedFail) => {}, + (4, AwaitingRemoteRevokeToAddForward) => {}, + (6, AwaitingRemoteRevokeToAddFail) => {}, + (8, Committed) => {}, + (10, AwaitingRemoteRevokeToRemoveFulfill) => {}, + (12, AwaitingRemoteRevokeToRemoveFail) => {}; +); + struct InboundHTLCOutput { htlc_id: u64, amount_msat: u64, @@ -166,6 +264,44 @@ struct InboundHTLCOutput { state: InboundHTLCState, } +/// Exposes details around pending inbound HTLCs. +#[derive(Clone, Debug, PartialEq)] +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. + /// See [InboundHTLCStateDetails] for information on the specific states. + pub state: InboundHTLCStateDetails, + /// 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. + /// This takes into account 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. + pub is_dust: bool, +} + +impl_writeable_tlv_based!(InboundHTLCDetails, { + (0, htlc_id, required), + (2, amount_msat, required), + (4, cltv_expiry, required), + (6, payment_hash, required), + (8, state, required), + (10, is_dust, required), +}); + #[cfg_attr(test, derive(Clone, Debug, PartialEq))] enum OutboundHTLCState { /// Added by us and included in a commitment_signed (if we were AwaitingRemoteRevoke when we @@ -199,6 +335,72 @@ enum OutboundHTLCState { AwaitingRemovedRemoteRevoke(OutboundHTLCOutcome), } +/// Exposes the state of pending outbound HTLCs. +/// +/// At a high level, an HTLC being forwarded from one Lightning node to another Lightning node goes +/// through the following states in the state machine: +/// - Announced for addition by the originating node through the update_add_htlc message. +/// - Added to the commitment transaction of the receiving node and originating node in turn +/// through the exchange of commitment_signed and revoke_and_ack messages. +/// - Announced for resolution (fulfillment or failure) by the receiving node through either one of +/// the update_fulfill_htlc, update_fail_htlc, and update_fail_malformed_htlc messages. +/// - Removed from the commitment transaction of the originating node and receiving node in turn +/// through the exchange of commitment_signed and revoke_and_ack messages. +/// +/// This can be used to inspect what next message an HTLC is waiting for to advance its state. +#[derive(Clone, Debug, PartialEq)] +pub enum OutboundHTLCStateDetails { + /// We are awaiting revoke_and_ack from the remote for previous changes before the HTLC can be + /// announced for addition with update_add_htlc on the remote's commitment transaction. + AwaitingRemoteRevokeToAdd, + /// The HTLC is included on the remote's commitment transaction through a commitment_signed and + /// revoke_and_ack exchange. + /// + /// 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 succesfully by the remote with a preimage in update_fulfill_htlc, + /// and we removed the HTLC from our commitment transaction through a commitment_signed and + /// revoke_and_ack exchange. 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 through a commitment_signed and + /// revoke_and_ack exchange. We are awaiting the appropriate revoke_and_ack's from the remote for + /// the removal from its commitment transaction. + AwaitingRemoteRevokeToRemoveFailure, +} + +impl From<&OutboundHTLCState> for OutboundHTLCStateDetails { + fn from(state: &OutboundHTLCState) -> OutboundHTLCStateDetails { + match state { + OutboundHTLCState::LocalAnnounced(_) => + OutboundHTLCStateDetails::AwaitingRemoteRevokeToAdd, + OutboundHTLCState::Committed => + OutboundHTLCStateDetails::Committed, + // RemoteRemoved states are ignored as the state is transient and the remote has not committed to + // the state yet. + OutboundHTLCState::RemoteRemoved(_) => + OutboundHTLCStateDetails::Committed, + OutboundHTLCState::AwaitingRemoteRevokeToRemove(OutboundHTLCOutcome::Success(_)) => + OutboundHTLCStateDetails::AwaitingRemoteRevokeToRemoveSuccess, + OutboundHTLCState::AwaitingRemoteRevokeToRemove(OutboundHTLCOutcome::Failure(_)) => + OutboundHTLCStateDetails::AwaitingRemoteRevokeToRemoveFailure, + OutboundHTLCState::AwaitingRemovedRemoteRevoke(OutboundHTLCOutcome::Success(_)) => + OutboundHTLCStateDetails::AwaitingRemoteRevokeToRemoveSuccess, + OutboundHTLCState::AwaitingRemovedRemoteRevoke(OutboundHTLCOutcome::Failure(_)) => + OutboundHTLCStateDetails::AwaitingRemoteRevokeToRemoveFailure, + } + } +} + +impl_writeable_tlv_based_enum!(OutboundHTLCStateDetails, + (0, AwaitingRemoteRevokeToAdd) => {}, + (2, Committed) => {}, + (4, AwaitingRemoteRevokeToRemoveSuccess) => {}, + (6, AwaitingRemoteRevokeToRemoveFailure) => {}; +); + #[derive(Clone)] #[cfg_attr(test, derive(Debug, PartialEq))] enum OutboundHTLCOutcome { @@ -237,6 +439,49 @@ struct OutboundHTLCOutput { skimmed_fee_msat: Option, } +/// Exposes details around pending outbound HTLCs. +#[derive(Clone, Debug, PartialEq)] +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. + /// See [OutboundHTLCStateDetails] for information on the specific states. + pub state: OutboundHTLCStateDetails, + /// 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. + /// This takes into account 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. + pub is_dust: bool, +} + +impl_writeable_tlv_based!(OutboundHTLCDetails, { + (0, htlc_id, required), + (2, amount_msat, required), + (4, cltv_expiry, required), + (6, payment_hash, required), + (8, state, required), + (10, skimmed_fee_msat, required), + (12, is_dust, required), +}); + /// See AwaitingRemoteRevoke ChannelState for more info #[cfg_attr(test, derive(Clone, Debug, PartialEq))] enum HTLCUpdateAwaitingACK { @@ -1966,6 +2211,90 @@ impl ChannelContext where SP::Target: SignerProvider { stats } + /// Returns information on all pending inbound HTLCs. + pub fn get_pending_inbound_htlc_details(&self) -> Vec { + let mut holding_cell_states = HashMap::new(); + for holding_cell_update in self.holding_cell_htlc_updates.iter() { + match holding_cell_update { + HTLCUpdateAwaitingACK::ClaimHTLC { htlc_id, .. } => { + holding_cell_states.insert( + htlc_id, + InboundHTLCStateDetails::AwaitingRemoteRevokeToRemoveFulfill, + ); + }, + HTLCUpdateAwaitingACK::FailHTLC { htlc_id, .. } => { + holding_cell_states.insert( + htlc_id, + InboundHTLCStateDetails::AwaitingRemoteRevokeToRemoveFail, + ); + }, + _ => {}, + } + } + let mut inbound_details = Vec::new(); + let htlc_success_dust_limit = if self.get_channel_type().supports_anchors_zero_fee_htlc_tx() { + 0 + } else { + let dust_buffer_feerate = self.get_dust_buffer_feerate(None) as u64; + dust_buffer_feerate * htlc_success_tx_weight(self.get_channel_type()) / 1000 + }; + let holder_dust_limit_success_sat = htlc_success_dust_limit + self.holder_dust_limit_satoshis; + for htlc in self.pending_inbound_htlcs.iter() { + inbound_details.push(InboundHTLCDetails{ + htlc_id: htlc.htlc_id, + amount_msat: htlc.amount_msat, + cltv_expiry: htlc.cltv_expiry, + payment_hash: htlc.payment_hash, + state: holding_cell_states.remove(&htlc.htlc_id).unwrap_or((&htlc.state).into()), + is_dust: htlc.amount_msat / 1000 < holder_dust_limit_success_sat, + }); + } + inbound_details + } + + /// Returns information on all pending outbound HTLCs. + pub fn get_pending_outbound_htlc_details(&self) -> Vec { + let mut outbound_details = Vec::new(); + let htlc_timeout_dust_limit = if self.get_channel_type().supports_anchors_zero_fee_htlc_tx() { + 0 + } else { + let dust_buffer_feerate = self.get_dust_buffer_feerate(None) as u64; + dust_buffer_feerate * htlc_success_tx_weight(self.get_channel_type()) / 1000 + }; + let holder_dust_limit_timeout_sat = htlc_timeout_dust_limit + self.holder_dust_limit_satoshis; + for htlc in self.pending_outbound_htlcs.iter() { + outbound_details.push(OutboundHTLCDetails{ + htlc_id: Some(htlc.htlc_id), + amount_msat: htlc.amount_msat, + cltv_expiry: htlc.cltv_expiry, + payment_hash: htlc.payment_hash, + skimmed_fee_msat: htlc.skimmed_fee_msat, + state: (&htlc.state).into(), + is_dust: htlc.amount_msat / 1000 < holder_dust_limit_timeout_sat, + }); + } + for holding_cell_update in self.holding_cell_htlc_updates.iter() { + if let HTLCUpdateAwaitingACK::AddHTLC { + amount_msat, + cltv_expiry, + payment_hash, + skimmed_fee_msat, + .. + } = *holding_cell_update { + outbound_details.push(OutboundHTLCDetails{ + htlc_id: None, + amount_msat: amount_msat, + cltv_expiry: cltv_expiry, + payment_hash: payment_hash, + skimmed_fee_msat: skimmed_fee_msat, + state: OutboundHTLCStateDetails::AwaitingRemoteRevokeToAdd, + is_dust: amount_msat / 1000 < holder_dust_limit_timeout_sat, + }); + } + } + outbound_details + } + /// Get the available balances, see [`AvailableBalances`]'s fields for more info. /// Doesn't bother handling the /// if-we-removed-it-already-but-haven't-fully-resolved-they-can-still-send-an-inbound-HTLC diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 87197dd4549..8b4786c8d53 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -44,6 +44,7 @@ use crate::events::{Event, EventHandler, EventsProvider, MessageSendEvent, Messa // construct one themselves. use crate::ln::{inbound_payment, ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channel::{self, Channel, ChannelPhase, ChannelContext, ChannelError, ChannelUpdateStatus, ShutdownResult, UnfundedChannelContext, UpdateFulfillCommitFetch, OutboundV1Channel, InboundV1Channel, WithChannelContext}; +pub use crate::ln::channel::{InboundHTLCDetails, InboundHTLCStateDetails, OutboundHTLCDetails, OutboundHTLCStateDetails}; use crate::ln::features::{Bolt12InvoiceFeatures, ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; #[cfg(any(feature = "_test_utils", test))] use crate::ln::features::Bolt11InvoiceFeatures; @@ -1793,6 +1794,14 @@ pub struct ChannelDetails { /// /// This field is only `None` for `ChannelDetails` objects serialized prior to LDK 0.0.109. pub config: Option, + /// Pending inbound HTLCs. + /// + /// This field is empty for objects serialized with LDK versions prior to 0.0.117. + pub pending_inbound_htlcs: Vec, + /// Pending outbound HTLCs. + /// + /// This field is empty for objects serialized with LDK versions prior to 0.0.117. + pub pending_outbound_htlcs: Vec, } impl ChannelDetails { @@ -1870,6 +1879,8 @@ impl ChannelDetails { inbound_htlc_maximum_msat: context.get_holder_htlc_maximum_msat(), config: Some(context.config()), channel_shutdown_state: Some(context.shutdown_state()), + pending_inbound_htlcs: context.get_pending_inbound_htlc_details(), + pending_outbound_htlcs: context.get_pending_outbound_htlc_details(), } } } @@ -9418,6 +9429,8 @@ impl Writeable for ChannelDetails { (37, user_channel_id_high_opt, option), (39, self.feerate_sat_per_1000_weight, option), (41, self.channel_shutdown_state, option), + (43, self.pending_inbound_htlcs, optional_vec), + (45, self.pending_outbound_htlcs, optional_vec), }); Ok(()) } @@ -9456,6 +9469,8 @@ impl Readable for ChannelDetails { (37, user_channel_id_high_opt, option), (39, feerate_sat_per_1000_weight, option), (41, channel_shutdown_state, option), + (43, pending_inbound_htlcs, optional_vec), + (45, pending_outbound_htlcs, optional_vec), }); // `user_channel_id` used to be a single u64 value. In order to remain backwards compatible with @@ -9492,6 +9507,8 @@ impl Readable for ChannelDetails { inbound_htlc_maximum_msat, feerate_sat_per_1000_weight, channel_shutdown_state, + pending_inbound_htlcs: pending_inbound_htlcs.unwrap_or(Vec::new()), + pending_outbound_htlcs: pending_outbound_htlcs.unwrap_or(Vec::new()), }) } } diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 436a37144b4..9f92f4ec501 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -3285,6 +3285,8 @@ mod tests { config: None, feerate_sat_per_1000_weight: None, channel_shutdown_state: Some(channelmanager::ChannelShutdownState::NotShuttingDown), + pending_inbound_htlcs: Vec::new(), + pending_outbound_htlcs: Vec::new(), } } @@ -8426,6 +8428,8 @@ pub(crate) mod bench_utils { config: None, feerate_sat_per_1000_weight: None, channel_shutdown_state: Some(channelmanager::ChannelShutdownState::NotShuttingDown), + pending_inbound_htlcs: Vec::new(), + pending_outbound_htlcs: Vec::new(), } }