Skip to content

Commit e180d94

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 d1daf45 commit e180d94

3 files changed

Lines changed: 167 additions & 55 deletions

File tree

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::{
@@ -1519,6 +1520,114 @@ pub enum ClosureReason {
15191520
},
15201521
}
15211522

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

src/types.rs

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ use bitcoin::{OutPoint, ScriptBuf};
2222

2323
use lightning::chain::chainmonitor;
2424
use lightning::impl_writeable_tlv_based;
25-
use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState};
25+
use lightning::ln::channel_state::{
26+
ChannelDetails as LdkChannelDetails, ChannelShutdownState, CounterpartyForwardingInfo,
27+
};
2628
use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress};
2729
use lightning::ln::peer_handler::IgnoringMessageHandler;
2830
use lightning::ln::types::ChannelId;
@@ -43,11 +45,17 @@ use crate::chain::ChainSource;
4345
use crate::config::ChannelConfig;
4446
use crate::data_store::DataStore;
4547
use crate::fee_estimator::OnchainFeeEstimator;
48+
use crate::ffi::maybe_wrap;
4649
use crate::logger::Logger;
4750
use crate::message_handler::NodeCustomMessageHandler;
4851
use crate::payment::{PaymentDetails, PendingPaymentDetails};
4952
use crate::runtime::RuntimeSpawner;
5053

54+
#[cfg(not(feature = "uniffi"))]
55+
type InitFeatures = lightning::types::features::InitFeatures;
56+
#[cfg(feature = "uniffi")]
57+
type InitFeatures = Arc<crate::ffi::InitFeatures>;
58+
5159
/// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the
5260
/// same time.
5361
pub trait SyncAndAsyncKVStore: KVStore + KVStoreSync {}
@@ -416,6 +424,34 @@ impl fmt::Display for UserChannelId {
416424
}
417425
}
418426

427+
/// Channel parameters which apply to our counterparty. These are split out from [`ChannelDetails`]
428+
/// to better separate parameters.
429+
#[derive(Clone, Debug, PartialEq)]
430+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
431+
pub struct ChannelCounterparty {
432+
/// The node_id of our counterparty
433+
pub node_id: PublicKey,
434+
/// The Features the channel counterparty provided upon last connection.
435+
/// Useful for routing as it is the most up-to-date copy of the counterparty's features and
436+
/// many routing-relevant features are present in the init context.
437+
pub features: InitFeatures,
438+
/// The value, in satoshis, that must always be held in the channel for our counterparty. This
439+
/// value ensures that if our counterparty broadcasts a revoked state, we can punish them by
440+
/// claiming at least this value on chain.
441+
///
442+
/// This value is not included in [`inbound_capacity_msat`] as it can never be spent.
443+
///
444+
/// [`inbound_capacity_msat`]: ChannelDetails::inbound_capacity_msat
445+
pub unspendable_punishment_reserve: u64,
446+
/// Information on the fees and requirements that the counterparty requires when forwarding
447+
/// payments to us through this channel.
448+
pub forwarding_info: Option<CounterpartyForwardingInfo>,
449+
/// The smallest value HTLC (in msat) the remote peer will accept, for this channel.
450+
pub outbound_htlc_minimum_msat: u64,
451+
/// The largest value HTLC (in msat) the remote peer currently will accept, for this channel.
452+
pub outbound_htlc_maximum_msat: Option<u64>,
453+
}
454+
419455
/// Details of a channel as returned by [`Node::list_channels`].
420456
///
421457
/// When a channel is spliced, most fields continue to refer to the original pre-splice channel
@@ -432,8 +468,8 @@ pub struct ChannelDetails {
432468
/// Note that this means this value is *not* persistent - it can change once during the
433469
/// lifetime of the channel.
434470
pub channel_id: ChannelId,
435-
/// The node ID of our the channel's counterparty.
436-
pub counterparty_node_id: PublicKey,
471+
/// Parameters which apply to our counterparty. See individual fields for more information.
472+
pub counterparty: ChannelCounterparty,
437473
/// The channel's funding transaction output, if we've negotiated the funding transaction with
438474
/// our counterparty already.
439475
///
@@ -549,28 +585,6 @@ pub struct ChannelDetails {
549585
/// The difference in the CLTV value between incoming HTLCs and an outbound HTLC forwarded over
550586
/// the channel.
551587
pub cltv_expiry_delta: Option<u16>,
552-
/// The value, in satoshis, that must always be held in the channel for our counterparty. This
553-
/// value ensures that if our counterparty broadcasts a revoked state, we can punish them by
554-
/// claiming at least this value on chain.
555-
///
556-
/// This value is not included in [`inbound_capacity_msat`] as it can never be spent.
557-
///
558-
/// [`inbound_capacity_msat`]: ChannelDetails::inbound_capacity_msat
559-
pub counterparty_unspendable_punishment_reserve: u64,
560-
/// The smallest value HTLC (in msat) the remote peer will accept, for this channel.
561-
///
562-
/// This field is only `None` before we have received either the `OpenChannel` or
563-
/// `AcceptChannel` message from the remote peer.
564-
pub counterparty_outbound_htlc_minimum_msat: Option<u64>,
565-
/// The largest value HTLC (in msat) the remote peer currently will accept, for this channel.
566-
pub counterparty_outbound_htlc_maximum_msat: Option<u64>,
567-
/// Base routing fee in millisatoshis.
568-
pub counterparty_forwarding_info_fee_base_msat: Option<u32>,
569-
/// Proportional fee, in millionths of a satoshi the channel will charge per transferred satoshi.
570-
pub counterparty_forwarding_info_fee_proportional_millionths: Option<u32>,
571-
/// The minimum difference in CLTV expiry between an ingoing HTLC and its outgoing counterpart,
572-
/// such that the outgoing HTLC is forwardable to this counterparty.
573-
pub counterparty_forwarding_info_cltv_expiry_delta: Option<u16>,
574588
/// The available outbound capacity for sending a single HTLC to the remote peer. This is
575589
/// similar to [`ChannelDetails::outbound_capacity_msat`] but it may be further restricted by
576590
/// the current state and per-HTLC limit(s). This is intended for use when routing, allowing us
@@ -608,7 +622,16 @@ impl From<LdkChannelDetails> for ChannelDetails {
608622
fn from(value: LdkChannelDetails) -> Self {
609623
ChannelDetails {
610624
channel_id: value.channel_id,
611-
counterparty_node_id: value.counterparty.node_id,
625+
counterparty: ChannelCounterparty {
626+
node_id: value.counterparty.node_id,
627+
features: maybe_wrap(value.counterparty.features),
628+
unspendable_punishment_reserve: value.counterparty.unspendable_punishment_reserve,
629+
forwarding_info: value.counterparty.forwarding_info,
630+
// unwrap safety: This value will be `None` for objects serialized with LDK versions
631+
// prior to 0.0.115.
632+
outbound_htlc_minimum_msat: value.counterparty.outbound_htlc_minimum_msat.unwrap(),
633+
outbound_htlc_maximum_msat: value.counterparty.outbound_htlc_maximum_msat,
634+
},
612635
funding_txo: value.funding_txo.map(|o| o.into_bitcoin_outpoint()),
613636
funding_redeem_script: value.funding_redeem_script,
614637
short_channel_id: value.short_channel_id,
@@ -629,26 +652,6 @@ impl From<LdkChannelDetails> for ChannelDetails {
629652
is_usable: value.is_usable,
630653
is_announced: value.is_announced,
631654
cltv_expiry_delta: value.config.map(|c| c.cltv_expiry_delta),
632-
counterparty_unspendable_punishment_reserve: value
633-
.counterparty
634-
.unspendable_punishment_reserve,
635-
counterparty_outbound_htlc_minimum_msat: value.counterparty.outbound_htlc_minimum_msat,
636-
counterparty_outbound_htlc_maximum_msat: value.counterparty.outbound_htlc_maximum_msat,
637-
counterparty_forwarding_info_fee_base_msat: value
638-
.counterparty
639-
.forwarding_info
640-
.as_ref()
641-
.map(|f| f.fee_base_msat),
642-
counterparty_forwarding_info_fee_proportional_millionths: value
643-
.counterparty
644-
.forwarding_info
645-
.as_ref()
646-
.map(|f| f.fee_proportional_millionths),
647-
counterparty_forwarding_info_cltv_expiry_delta: value
648-
.counterparty
649-
.forwarding_info
650-
.as_ref()
651-
.map(|f| f.cltv_expiry_delta),
652655
next_outbound_htlc_limit_msat: value.next_outbound_htlc_limit_msat,
653656
next_outbound_htlc_minimum_msat: value.next_outbound_htlc_minimum_msat,
654657
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
@@ -2197,7 +2197,7 @@ async fn lsps2_client_trusts_lsp() {
21972197
client_node
21982198
.list_channels()
21992199
.iter()
2200-
.find(|c| c.counterparty_node_id == service_node_id)
2200+
.find(|c| c.counterparty.node_id == service_node_id)
22012201
.unwrap()
22022202
.confirmations,
22032203
Some(0)
@@ -2206,7 +2206,7 @@ async fn lsps2_client_trusts_lsp() {
22062206
service_node
22072207
.list_channels()
22082208
.iter()
2209-
.find(|c| c.counterparty_node_id == client_node_id)
2209+
.find(|c| c.counterparty.node_id == client_node_id)
22102210
.unwrap()
22112211
.confirmations,
22122212
Some(0)
@@ -2241,7 +2241,7 @@ async fn lsps2_client_trusts_lsp() {
22412241
client_node
22422242
.list_channels()
22432243
.iter()
2244-
.find(|c| c.counterparty_node_id == service_node_id)
2244+
.find(|c| c.counterparty.node_id == service_node_id)
22452245
.unwrap()
22462246
.confirmations,
22472247
Some(6)
@@ -2250,7 +2250,7 @@ async fn lsps2_client_trusts_lsp() {
22502250
service_node
22512251
.list_channels()
22522252
.iter()
2253-
.find(|c| c.counterparty_node_id == client_node_id)
2253+
.find(|c| c.counterparty.node_id == client_node_id)
22542254
.unwrap()
22552255
.confirmations,
22562256
Some(6)
@@ -2370,7 +2370,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() {
23702370
client_node
23712371
.list_channels()
23722372
.iter()
2373-
.find(|c| c.counterparty_node_id == service_node_id)
2373+
.find(|c| c.counterparty.node_id == service_node_id)
23742374
.unwrap()
23752375
.confirmations,
23762376
Some(6)
@@ -2379,7 +2379,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() {
23792379
service_node
23802380
.list_channels()
23812381
.iter()
2382-
.find(|c| c.counterparty_node_id == client_node_id)
2382+
.find(|c| c.counterparty.node_id == client_node_id)
23832383
.unwrap()
23842384
.confirmations,
23852385
Some(6)
@@ -2822,7 +2822,7 @@ async fn open_channel_with_all_with_anchors() {
28222822
assert_eq!(channels.len(), 1);
28232823
let channel = &channels[0];
28242824
assert!(channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 500);
2825-
assert_eq!(channel.counterparty_node_id, node_b.node_id());
2825+
assert_eq!(channel.counterparty.node_id, node_b.node_id());
28262826
assert_eq!(channel.funding_txo.unwrap(), funding_txo);
28272827

28282828
node_a.stop().unwrap();
@@ -2873,7 +2873,7 @@ async fn open_channel_with_all_without_anchors() {
28732873
assert_eq!(channels.len(), 1);
28742874
let channel = &channels[0];
28752875
assert!(channel.channel_value_sats > premine_amount_sat - 500);
2876-
assert_eq!(channel.counterparty_node_id, node_b.node_id());
2876+
assert_eq!(channel.counterparty.node_id, node_b.node_id());
28772877
assert_eq!(channel.funding_txo.unwrap(), funding_txo);
28782878

28792879
node_a.stop().unwrap();

0 commit comments

Comments
 (0)