From 7a44af3a4af516df864ad7c75a510e2c14773ceb Mon Sep 17 00:00:00 2001 From: Willem Van Lint Date: Fri, 4 Aug 2023 17:07:04 -0700 Subject: [PATCH] Include pending HTLCs in ChannelDetails This exposes details around pending HTLCs in ChannelDetails. The state of the HTLC in the state machine is also included, so it can be determined which protocol message the HTLC is waiting for to advance. --- fuzz/src/router.rs | 2 + lightning/src/ln/channel.rs | 329 +++++++++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 17 ++ lightning/src/routing/router.rs | 4 + 4 files changed, 352 insertions(+) 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(), } }