Skip to content

Commit 88ae45f

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 fadc74f commit 88ae45f

File tree

3 files changed

+165
-53
lines changed

3 files changed

+165
-53
lines changed

src/ffi/types.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +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::CounterpartyForwardingInfo;
2829
use lightning::ln::channelmanager::PaymentId;
2930
use lightning::ln::msgs::DecodeError;
3031
pub use lightning::ln::types::ChannelId;
@@ -43,6 +44,7 @@ pub use lightning_liquidity::lsps0::ser::LSPSDateTime;
4344
pub use lightning_liquidity::lsps1::msgs::{
4445
LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentState,
4546
};
47+
use lightning_types::features::InitFeatures as LdkInitFeatures;
4648
pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
4749
pub use lightning_types::string::UntrustedString;
4850
use vss_client::headers::{
@@ -1496,6 +1498,114 @@ pub enum ClosureReason {
14961498
},
14971499
}
14981500

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

src/types.rs

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECH
1616
use lightning::chain::chainmonitor;
1717
use lightning::impl_writeable_tlv_based;
1818
use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails;
19+
use lightning::ln::channel_state::CounterpartyForwardingInfo;
1920
use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress};
2021
use lightning::ln::peer_handler::IgnoringMessageHandler;
2122
use lightning::ln::types::ChannelId;
@@ -35,11 +36,17 @@ use crate::chain::ChainSource;
3536
use crate::config::ChannelConfig;
3637
use crate::data_store::DataStore;
3738
use crate::fee_estimator::OnchainFeeEstimator;
39+
use crate::ffi::maybe_wrap;
3840
use crate::logger::Logger;
3941
use crate::message_handler::NodeCustomMessageHandler;
4042
use crate::payment::{PaymentDetails, PendingPaymentDetails};
4143
use crate::runtime::RuntimeSpawner;
4244

45+
#[cfg(not(feature = "uniffi"))]
46+
type InitFeatures = lightning::types::features::InitFeatures;
47+
#[cfg(feature = "uniffi")]
48+
type InitFeatures = Arc<crate::ffi::InitFeatures>;
49+
4350
/// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the
4451
/// same time.
4552
pub trait SyncAndAsyncKVStore: KVStore + KVStoreSync {}
@@ -376,6 +383,34 @@ impl fmt::Display for UserChannelId {
376383
}
377384
}
378385

386+
/// Channel parameters which apply to our counterparty. These are split out from [`ChannelDetails`]
387+
/// to better separate parameters.
388+
#[derive(Clone, Debug, PartialEq)]
389+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
390+
pub struct ChannelCounterparty {
391+
/// The node_id of our counterparty
392+
pub node_id: PublicKey,
393+
/// The Features the channel counterparty provided upon last connection.
394+
/// Useful for routing as it is the most up-to-date copy of the counterparty's features and
395+
/// many routing-relevant features are present in the init context.
396+
pub features: InitFeatures,
397+
/// The value, in satoshis, that must always be held in the channel for our counterparty. This
398+
/// value ensures that if our counterparty broadcasts a revoked state, we can punish them by
399+
/// claiming at least this value on chain.
400+
///
401+
/// This value is not included in [`inbound_capacity_msat`] as it can never be spent.
402+
///
403+
/// [`inbound_capacity_msat`]: ChannelDetails::inbound_capacity_msat
404+
pub unspendable_punishment_reserve: u64,
405+
/// Information on the fees and requirements that the counterparty requires when forwarding
406+
/// payments to us through this channel.
407+
pub forwarding_info: Option<CounterpartyForwardingInfo>,
408+
/// The smallest value HTLC (in msat) the remote peer will accept, for this channel.
409+
pub outbound_htlc_minimum_msat: u64,
410+
/// The largest value HTLC (in msat) the remote peer currently will accept, for this channel.
411+
pub outbound_htlc_maximum_msat: Option<u64>,
412+
}
413+
379414
/// Details of a channel as returned by [`Node::list_channels`].
380415
///
381416
/// When a channel is spliced, most fields continue to refer to the original pre-splice channel
@@ -392,8 +427,8 @@ pub struct ChannelDetails {
392427
/// Note that this means this value is *not* persistent - it can change once during the
393428
/// lifetime of the channel.
394429
pub channel_id: ChannelId,
395-
/// The node ID of our the channel's counterparty.
396-
pub counterparty_node_id: PublicKey,
430+
/// Parameters which apply to our counterparty. See individual fields for more information.
431+
pub counterparty: ChannelCounterparty,
397432
/// The channel's funding transaction output, if we've negotiated the funding transaction with
398433
/// our counterparty already.
399434
///
@@ -509,28 +544,6 @@ pub struct ChannelDetails {
509544
/// The difference in the CLTV value between incoming HTLCs and an outbound HTLC forwarded over
510545
/// the channel.
511546
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>,
534547
/// The available outbound capacity for sending a single HTLC to the remote peer. This is
535548
/// similar to [`ChannelDetails::outbound_capacity_msat`] but it may be further restricted by
536549
/// the current state and per-HTLC limit(s). This is intended for use when routing, allowing us
@@ -564,7 +577,16 @@ impl From<LdkChannelDetails> for ChannelDetails {
564577
fn from(value: LdkChannelDetails) -> Self {
565578
ChannelDetails {
566579
channel_id: value.channel_id,
567-
counterparty_node_id: value.counterparty.node_id,
580+
counterparty: ChannelCounterparty {
581+
node_id: value.counterparty.node_id,
582+
features: maybe_wrap(value.counterparty.features),
583+
unspendable_punishment_reserve: value.counterparty.unspendable_punishment_reserve,
584+
forwarding_info: value.counterparty.forwarding_info,
585+
// unwrap safety: This value will be `None` for objects serialized with LDK versions
586+
// prior to 0.0.115.
587+
outbound_htlc_minimum_msat: value.counterparty.outbound_htlc_minimum_msat.unwrap(),
588+
outbound_htlc_maximum_msat: value.counterparty.outbound_htlc_maximum_msat,
589+
},
568590
funding_txo: value.funding_txo.map(|o| o.into_bitcoin_outpoint()),
569591
funding_redeem_script: value.funding_redeem_script,
570592
short_channel_id: value.short_channel_id,
@@ -585,26 +607,6 @@ impl From<LdkChannelDetails> for ChannelDetails {
585607
is_usable: value.is_usable,
586608
is_announced: value.is_announced,
587609
cltv_expiry_delta: value.config.map(|c| c.cltv_expiry_delta),
588-
counterparty_unspendable_punishment_reserve: value
589-
.counterparty
590-
.unspendable_punishment_reserve,
591-
counterparty_outbound_htlc_minimum_msat: value.counterparty.outbound_htlc_minimum_msat,
592-
counterparty_outbound_htlc_maximum_msat: value.counterparty.outbound_htlc_maximum_msat,
593-
counterparty_forwarding_info_fee_base_msat: value
594-
.counterparty
595-
.forwarding_info
596-
.as_ref()
597-
.map(|f| f.fee_base_msat),
598-
counterparty_forwarding_info_fee_proportional_millionths: value
599-
.counterparty
600-
.forwarding_info
601-
.as_ref()
602-
.map(|f| f.fee_proportional_millionths),
603-
counterparty_forwarding_info_cltv_expiry_delta: value
604-
.counterparty
605-
.forwarding_info
606-
.as_ref()
607-
.map(|f| f.cltv_expiry_delta),
608610
next_outbound_htlc_limit_msat: value.next_outbound_htlc_limit_msat,
609611
next_outbound_htlc_minimum_msat: value.next_outbound_htlc_minimum_msat,
610612
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
@@ -2114,7 +2114,7 @@ async fn lsps2_client_trusts_lsp() {
21142114
client_node
21152115
.list_channels()
21162116
.iter()
2117-
.find(|c| c.counterparty_node_id == service_node_id)
2117+
.find(|c| c.counterparty.node_id == service_node_id)
21182118
.unwrap()
21192119
.confirmations,
21202120
Some(0)
@@ -2123,7 +2123,7 @@ async fn lsps2_client_trusts_lsp() {
21232123
service_node
21242124
.list_channels()
21252125
.iter()
2126-
.find(|c| c.counterparty_node_id == client_node_id)
2126+
.find(|c| c.counterparty.node_id == client_node_id)
21272127
.unwrap()
21282128
.confirmations,
21292129
Some(0)
@@ -2158,7 +2158,7 @@ async fn lsps2_client_trusts_lsp() {
21582158
client_node
21592159
.list_channels()
21602160
.iter()
2161-
.find(|c| c.counterparty_node_id == service_node_id)
2161+
.find(|c| c.counterparty.node_id == service_node_id)
21622162
.unwrap()
21632163
.confirmations,
21642164
Some(6)
@@ -2167,7 +2167,7 @@ async fn lsps2_client_trusts_lsp() {
21672167
service_node
21682168
.list_channels()
21692169
.iter()
2170-
.find(|c| c.counterparty_node_id == client_node_id)
2170+
.find(|c| c.counterparty.node_id == client_node_id)
21712171
.unwrap()
21722172
.confirmations,
21732173
Some(6)
@@ -2286,7 +2286,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() {
22862286
client_node
22872287
.list_channels()
22882288
.iter()
2289-
.find(|c| c.counterparty_node_id == service_node_id)
2289+
.find(|c| c.counterparty.node_id == service_node_id)
22902290
.unwrap()
22912291
.confirmations,
22922292
Some(6)
@@ -2295,7 +2295,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() {
22952295
service_node
22962296
.list_channels()
22972297
.iter()
2298-
.find(|c| c.counterparty_node_id == client_node_id)
2298+
.find(|c| c.counterparty.node_id == client_node_id)
22992299
.unwrap()
23002300
.confirmations,
23012301
Some(6)
@@ -2738,7 +2738,7 @@ async fn open_channel_with_all_with_anchors() {
27382738
assert_eq!(channels.len(), 1);
27392739
let channel = &channels[0];
27402740
assert!(channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 500);
2741-
assert_eq!(channel.counterparty_node_id, node_b.node_id());
2741+
assert_eq!(channel.counterparty.node_id, node_b.node_id());
27422742
assert_eq!(channel.funding_txo.unwrap(), funding_txo);
27432743

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

27952795
node_a.stop().unwrap();

0 commit comments

Comments
 (0)