Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 137 additions & 1 deletion src/ffi/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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.
/// 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<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<OutboundHTLCStateDetails>,
/// The extra fee being skimmed off the top of this HTLC.
pub skimmed_fee_msat: Option<u64>,
/// 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)]
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<ChannelTypeFeatures>,
/// Pending inbound HTLCs.
pub pending_inbound_htlcs: Vec<InboundHTLCDetails>,
/// Pending outbound HTLCs.
pub pending_outbound_htlcs: Vec<OutboundHTLCDetails>,
/// 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<u64>,
}

impl ChannelDetails {
Expand Down Expand Up @@ -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
Expand All @@ -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,
}
}
}
Expand Down
111 changes: 111 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading