Skip to content

Commit d0cd95a

Browse files
committed
Expose per-channel features in ChannelDetails
We previously flattened ChannelCounterparty fields into ChannelDetails as individual counterparty_* fields, and InitFeatures was entirely omitted. This made it impossible for consumers to access per-peer feature flags, and awkward to access counterparty forwarding information without navigating the flattened field names. This commit replaces the flattened fields with a structured ChannelCounterparty type that mirrors LDK's ChannelCounterparty, exposing InitFeatures and CounterpartyForwardingInfo that were previously inaccessible. Breaking change!
1 parent fe692f3 commit d0cd95a

File tree

3 files changed

+167
-55
lines changed

3 files changed

+167
-55
lines changed

src/ffi/types.rs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub use bitcoin::{Address, BlockHash, Network, OutPoint, ScriptBuf, Txid};
2525
pub use lightning::chain::channelmonitor::BalanceSource;
2626
use lightning::events::PaidBolt12Invoice as LdkPaidBolt12Invoice;
2727
pub use lightning::events::{ClosureReason, PaymentFailureReason};
28-
use lightning::ln::channel_state::ChannelShutdownState;
28+
use lightning::ln::channel_state::{ChannelShutdownState, CounterpartyForwardingInfo};
2929
use lightning::ln::channelmanager::PaymentId;
3030
use lightning::ln::msgs::DecodeError;
3131
pub use lightning::ln::types::ChannelId;
@@ -44,6 +44,7 @@ pub use lightning_liquidity::lsps0::ser::LSPSDateTime;
4444
pub use lightning_liquidity::lsps1::msgs::{
4545
LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentState,
4646
};
47+
use lightning_types::features::InitFeatures as LdkInitFeatures;
4748
pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
4849
pub use lightning_types::string::UntrustedString;
4950
use vss_client::headers::{
@@ -1517,6 +1518,114 @@ pub enum ClosureReason {
15171518
},
15181519
}
15191520

1521+
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)]
1522+
#[uniffi::export(Debug, Eq)]
1523+
pub struct InitFeatures {
1524+
pub(crate) inner: LdkInitFeatures,
1525+
}
1526+
1527+
impl InitFeatures {
1528+
/// Constructs init features from big-endian BOLT 9 encoded bytes.
1529+
#[uniffi::constructor]
1530+
pub fn from_bytes(bytes: &[u8]) -> Self {
1531+
Self { inner: LdkInitFeatures::from_be_bytes(bytes.to_vec()).into() }
1532+
}
1533+
1534+
/// Returns the BOLT 9 big-endian encoded representation of these features.
1535+
pub fn to_bytes(&self) -> Vec<u8> {
1536+
self.inner.encode()
1537+
}
1538+
1539+
/// Whether the peer supports `option_static_remotekey` (bit 13).
1540+
///
1541+
/// This ensures the non-broadcaster's output pays directly to their specified key,
1542+
/// simplifying recovery if a channel is force-closed.
1543+
pub fn supports_static_remote_key(&self) -> bool {
1544+
self.inner.supports_static_remote_key()
1545+
}
1546+
1547+
/// Whether the peer supports `option_anchors_zero_fee_htlc_tx` (bit 23).
1548+
///
1549+
/// Anchor channels allow fee-bumping commitment transactions after broadcast,
1550+
/// improving on-chain fee management.
1551+
pub fn supports_anchors_zero_fee_htlc_tx(&self) -> bool {
1552+
self.inner.supports_anchors_zero_fee_htlc_tx()
1553+
}
1554+
1555+
/// Whether the peer supports `option_support_large_channel` (bit 19).
1556+
///
1557+
/// When supported, channels larger than 2^24 satoshis (≈0.168 BTC) may be opened.
1558+
pub fn supports_wumbo(&self) -> bool {
1559+
self.inner.supports_wumbo()
1560+
}
1561+
1562+
/// Whether the peer supports `option_route_blinding` (bit 25).
1563+
///
1564+
/// Route blinding allows the recipient to hide their node identity and
1565+
/// last-hop channel from the sender.
1566+
pub fn supports_route_blinding(&self) -> bool {
1567+
self.inner.supports_route_blinding()
1568+
}
1569+
1570+
/// Whether the peer supports `option_onion_messages` (bit 39).
1571+
///
1572+
/// Onion messages enable communication over the Lightning Network without
1573+
/// requiring a payment, used by BOLT 12 offers and async payments.
1574+
pub fn supports_onion_messages(&self) -> bool {
1575+
self.inner.supports_onion_messages()
1576+
}
1577+
1578+
/// Whether the peer supports `option_scid_alias` (bit 47).
1579+
///
1580+
/// When supported, the peer will only forward using short channel ID aliases,
1581+
/// preventing the real channel UTXO from being revealed during routing.
1582+
pub fn supports_scid_privacy(&self) -> bool {
1583+
self.inner.supports_scid_privacy()
1584+
}
1585+
1586+
/// Whether the peer supports `option_zeroconf` (bit 51).
1587+
///
1588+
/// Zero-conf channels can be used immediately without waiting for
1589+
/// on-chain funding confirmations.
1590+
pub fn supports_zero_conf(&self) -> bool {
1591+
self.inner.requires_zero_conf()
1592+
}
1593+
1594+
/// Whether the peer supports `option_dual_fund` (bit 29).
1595+
///
1596+
/// Dual-funded channels allow both parties to contribute funds
1597+
/// to the channel opening transaction.
1598+
pub fn supports_dual_fund(&self) -> bool {
1599+
self.inner.supports_dual_fund()
1600+
}
1601+
1602+
/// Whether the peer supports `option_quiesce` (bit 35).
1603+
///
1604+
/// Quiescence is a prerequisite for splicing, allowing both sides to
1605+
/// pause HTLC activity before modifying the funding transaction.
1606+
pub fn supports_quiescence(&self) -> bool {
1607+
self.inner.supports_quiescence()
1608+
}
1609+
}
1610+
1611+
impl From<LdkInitFeatures> for InitFeatures {
1612+
fn from(ldk_init: LdkInitFeatures) -> Self {
1613+
Self { inner: ldk_init }
1614+
}
1615+
}
1616+
1617+
/// Information needed for constructing an invoice route hint for this channel.
1618+
#[uniffi::remote(Record)]
1619+
pub struct CounterpartyForwardingInfo {
1620+
/// Base routing fee in millisatoshis.
1621+
pub fee_base_msat: u32,
1622+
/// Amount in millionths of a satoshi the channel will charge per transferred satoshi.
1623+
pub fee_proportional_millionths: u32,
1624+
/// The minimum difference in cltv_expiry between an ingoing HTLC and its outgoing counterpart,
1625+
/// such that the outgoing HTLC is forwardable to this counterparty.
1626+
pub cltv_expiry_delta: u16,
1627+
}
1628+
15201629
#[cfg(test)]
15211630
mod tests {
15221631
use std::num::NonZeroU64;

src/types.rs

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ use bitcoin::{OutPoint, ScriptBuf};
1515
use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver;
1616
use lightning::chain::chainmonitor;
1717
use lightning::impl_writeable_tlv_based;
18-
use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState};
18+
use lightning::ln::channel_state::{
19+
ChannelDetails as LdkChannelDetails, ChannelShutdownState, CounterpartyForwardingInfo,
20+
};
1921
use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress};
2022
use lightning::ln::peer_handler::IgnoringMessageHandler;
2123
use lightning::ln::types::ChannelId;
@@ -35,11 +37,17 @@ use crate::chain::ChainSource;
3537
use crate::config::ChannelConfig;
3638
use crate::data_store::DataStore;
3739
use crate::fee_estimator::OnchainFeeEstimator;
40+
use crate::ffi::maybe_wrap;
3841
use crate::logger::Logger;
3942
use crate::message_handler::NodeCustomMessageHandler;
4043
use crate::payment::{PaymentDetails, PendingPaymentDetails};
4144
use crate::runtime::RuntimeSpawner;
4245

46+
#[cfg(not(feature = "uniffi"))]
47+
type InitFeatures = lightning::types::features::InitFeatures;
48+
#[cfg(feature = "uniffi")]
49+
type InitFeatures = Arc<crate::ffi::InitFeatures>;
50+
4351
/// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the
4452
/// same time.
4553
pub trait SyncAndAsyncKVStore: KVStore + KVStoreSync {}
@@ -376,6 +384,34 @@ impl fmt::Display for UserChannelId {
376384
}
377385
}
378386

387+
/// Channel parameters which apply to our counterparty. These are split out from [`ChannelDetails`]
388+
/// to better separate parameters.
389+
#[derive(Clone, Debug, PartialEq)]
390+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
391+
pub struct ChannelCounterparty {
392+
/// The node_id of our counterparty
393+
pub node_id: PublicKey,
394+
/// The Features the channel counterparty provided upon last connection.
395+
/// Useful for routing as it is the most up-to-date copy of the counterparty's features and
396+
/// many routing-relevant features are present in the init context.
397+
pub features: InitFeatures,
398+
/// The value, in satoshis, that must always be held in the channel for our counterparty. This
399+
/// value ensures that if our counterparty broadcasts a revoked state, we can punish them by
400+
/// claiming at least this value on chain.
401+
///
402+
/// This value is not included in [`inbound_capacity_msat`] as it can never be spent.
403+
///
404+
/// [`inbound_capacity_msat`]: ChannelDetails::inbound_capacity_msat
405+
pub unspendable_punishment_reserve: u64,
406+
/// Information on the fees and requirements that the counterparty requires when forwarding
407+
/// payments to us through this channel.
408+
pub forwarding_info: Option<CounterpartyForwardingInfo>,
409+
/// The smallest value HTLC (in msat) the remote peer will accept, for this channel.
410+
pub outbound_htlc_minimum_msat: u64,
411+
/// The largest value HTLC (in msat) the remote peer currently will accept, for this channel.
412+
pub outbound_htlc_maximum_msat: Option<u64>,
413+
}
414+
379415
/// Details of a channel as returned by [`Node::list_channels`].
380416
///
381417
/// When a channel is spliced, most fields continue to refer to the original pre-splice channel
@@ -392,8 +428,8 @@ pub struct ChannelDetails {
392428
/// Note that this means this value is *not* persistent - it can change once during the
393429
/// lifetime of the channel.
394430
pub channel_id: ChannelId,
395-
/// The node ID of our the channel's counterparty.
396-
pub counterparty_node_id: PublicKey,
431+
/// Parameters which apply to our counterparty. See individual fields for more information.
432+
pub counterparty: ChannelCounterparty,
397433
/// The channel's funding transaction output, if we've negotiated the funding transaction with
398434
/// our counterparty already.
399435
///
@@ -509,28 +545,6 @@ pub struct ChannelDetails {
509545
/// The difference in the CLTV value between incoming HTLCs and an outbound HTLC forwarded over
510546
/// the channel.
511547
pub cltv_expiry_delta: Option<u16>,
512-
/// The value, in satoshis, that must always be held in the channel for our counterparty. This
513-
/// value ensures that if our counterparty broadcasts a revoked state, we can punish them by
514-
/// claiming at least this value on chain.
515-
///
516-
/// This value is not included in [`inbound_capacity_msat`] as it can never be spent.
517-
///
518-
/// [`inbound_capacity_msat`]: ChannelDetails::inbound_capacity_msat
519-
pub counterparty_unspendable_punishment_reserve: u64,
520-
/// The smallest value HTLC (in msat) the remote peer will accept, for this channel.
521-
///
522-
/// This field is only `None` before we have received either the `OpenChannel` or
523-
/// `AcceptChannel` message from the remote peer.
524-
pub counterparty_outbound_htlc_minimum_msat: Option<u64>,
525-
/// The largest value HTLC (in msat) the remote peer currently will accept, for this channel.
526-
pub counterparty_outbound_htlc_maximum_msat: Option<u64>,
527-
/// Base routing fee in millisatoshis.
528-
pub counterparty_forwarding_info_fee_base_msat: Option<u32>,
529-
/// Proportional fee, in millionths of a satoshi the channel will charge per transferred satoshi.
530-
pub counterparty_forwarding_info_fee_proportional_millionths: Option<u32>,
531-
/// The minimum difference in CLTV expiry between an ingoing HTLC and its outgoing counterpart,
532-
/// such that the outgoing HTLC is forwardable to this counterparty.
533-
pub counterparty_forwarding_info_cltv_expiry_delta: Option<u16>,
534548
/// The available outbound capacity for sending a single HTLC to the remote peer. This is
535549
/// similar to [`ChannelDetails::outbound_capacity_msat`] but it may be further restricted by
536550
/// the current state and per-HTLC limit(s). This is intended for use when routing, allowing us
@@ -568,7 +582,16 @@ impl From<LdkChannelDetails> for ChannelDetails {
568582
fn from(value: LdkChannelDetails) -> Self {
569583
ChannelDetails {
570584
channel_id: value.channel_id,
571-
counterparty_node_id: value.counterparty.node_id,
585+
counterparty: ChannelCounterparty {
586+
node_id: value.counterparty.node_id,
587+
features: maybe_wrap(value.counterparty.features),
588+
unspendable_punishment_reserve: value.counterparty.unspendable_punishment_reserve,
589+
forwarding_info: value.counterparty.forwarding_info,
590+
// unwrap safety: This value will be `None` for objects serialized with LDK versions
591+
// prior to 0.0.115.
592+
outbound_htlc_minimum_msat: value.counterparty.outbound_htlc_minimum_msat.unwrap(),
593+
outbound_htlc_maximum_msat: value.counterparty.outbound_htlc_maximum_msat,
594+
},
572595
funding_txo: value.funding_txo.map(|o| o.into_bitcoin_outpoint()),
573596
funding_redeem_script: value.funding_redeem_script,
574597
short_channel_id: value.short_channel_id,
@@ -589,26 +612,6 @@ impl From<LdkChannelDetails> for ChannelDetails {
589612
is_usable: value.is_usable,
590613
is_announced: value.is_announced,
591614
cltv_expiry_delta: value.config.map(|c| c.cltv_expiry_delta),
592-
counterparty_unspendable_punishment_reserve: value
593-
.counterparty
594-
.unspendable_punishment_reserve,
595-
counterparty_outbound_htlc_minimum_msat: value.counterparty.outbound_htlc_minimum_msat,
596-
counterparty_outbound_htlc_maximum_msat: value.counterparty.outbound_htlc_maximum_msat,
597-
counterparty_forwarding_info_fee_base_msat: value
598-
.counterparty
599-
.forwarding_info
600-
.as_ref()
601-
.map(|f| f.fee_base_msat),
602-
counterparty_forwarding_info_fee_proportional_millionths: value
603-
.counterparty
604-
.forwarding_info
605-
.as_ref()
606-
.map(|f| f.fee_proportional_millionths),
607-
counterparty_forwarding_info_cltv_expiry_delta: value
608-
.counterparty
609-
.forwarding_info
610-
.as_ref()
611-
.map(|f| f.cltv_expiry_delta),
612615
next_outbound_htlc_limit_msat: value.next_outbound_htlc_limit_msat,
613616
next_outbound_htlc_minimum_msat: value.next_outbound_htlc_minimum_msat,
614617
force_close_spend_delay: value.force_close_spend_delay,

tests/integration_tests_rust.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,7 +2116,7 @@ async fn lsps2_client_trusts_lsp() {
21162116
client_node
21172117
.list_channels()
21182118
.iter()
2119-
.find(|c| c.counterparty_node_id == service_node_id)
2119+
.find(|c| c.counterparty.node_id == service_node_id)
21202120
.unwrap()
21212121
.confirmations,
21222122
Some(0)
@@ -2125,7 +2125,7 @@ async fn lsps2_client_trusts_lsp() {
21252125
service_node
21262126
.list_channels()
21272127
.iter()
2128-
.find(|c| c.counterparty_node_id == client_node_id)
2128+
.find(|c| c.counterparty.node_id == client_node_id)
21292129
.unwrap()
21302130
.confirmations,
21312131
Some(0)
@@ -2160,7 +2160,7 @@ async fn lsps2_client_trusts_lsp() {
21602160
client_node
21612161
.list_channels()
21622162
.iter()
2163-
.find(|c| c.counterparty_node_id == service_node_id)
2163+
.find(|c| c.counterparty.node_id == service_node_id)
21642164
.unwrap()
21652165
.confirmations,
21662166
Some(6)
@@ -2169,7 +2169,7 @@ async fn lsps2_client_trusts_lsp() {
21692169
service_node
21702170
.list_channels()
21712171
.iter()
2172-
.find(|c| c.counterparty_node_id == client_node_id)
2172+
.find(|c| c.counterparty.node_id == client_node_id)
21732173
.unwrap()
21742174
.confirmations,
21752175
Some(6)
@@ -2289,7 +2289,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() {
22892289
client_node
22902290
.list_channels()
22912291
.iter()
2292-
.find(|c| c.counterparty_node_id == service_node_id)
2292+
.find(|c| c.counterparty.node_id == service_node_id)
22932293
.unwrap()
22942294
.confirmations,
22952295
Some(6)
@@ -2298,7 +2298,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() {
22982298
service_node
22992299
.list_channels()
23002300
.iter()
2301-
.find(|c| c.counterparty_node_id == client_node_id)
2301+
.find(|c| c.counterparty.node_id == client_node_id)
23022302
.unwrap()
23032303
.confirmations,
23042304
Some(6)
@@ -2741,7 +2741,7 @@ async fn open_channel_with_all_with_anchors() {
27412741
assert_eq!(channels.len(), 1);
27422742
let channel = &channels[0];
27432743
assert!(channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 500);
2744-
assert_eq!(channel.counterparty_node_id, node_b.node_id());
2744+
assert_eq!(channel.counterparty.node_id, node_b.node_id());
27452745
assert_eq!(channel.funding_txo.unwrap(), funding_txo);
27462746

27472747
node_a.stop().unwrap();
@@ -2792,7 +2792,7 @@ async fn open_channel_with_all_without_anchors() {
27922792
assert_eq!(channels.len(), 1);
27932793
let channel = &channels[0];
27942794
assert!(channel.channel_value_sats > premine_amount_sat - 500);
2795-
assert_eq!(channel.counterparty_node_id, node_b.node_id());
2795+
assert_eq!(channel.counterparty.node_id, node_b.node_id());
27962796
assert_eq!(channel.funding_txo.unwrap(), funding_txo);
27972797

27982798
node_a.stop().unwrap();

0 commit comments

Comments
 (0)