From ead6a8cd67c0aae96ec200f4a2b5ac25298c8aba Mon Sep 17 00:00:00 2001 From: Enigbe Date: Thu, 26 Feb 2026 13:41:08 +0100 Subject: [PATCH 1/2] Expose node announcement feature flags Expose the feature flags advertised by the node in its node_announcement message via NodeStatus::node_features. For UniFFI consumers, expose NodeFeatures as an object with BOLT 9 byte encoding helpers and typed feature accessors. This intentionally avoids exposing freestanding Node methods for init, channel, invoice, or node feature contexts. Channel and invoice features are context-specific, and exposing node-level helpers for them could be confused with negotiated per-peer, per-channel, or per-invoice features. Those negotiated feature surfaces are handled separately. --- src/ffi/types.rs | 8 ++++++++ src/lib.rs | 30 +++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/ffi/types.rs b/src/ffi/types.rs index ad293bc3e0..9e8886c647 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -44,6 +44,7 @@ pub use lightning_liquidity::lsps0::ser::LSPSDateTime; pub use lightning_liquidity::lsps1::msgs::{ LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentState, }; +use lightning_types::features::NodeFeatures; pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning_types::string::UntrustedString; use vss_client::headers::{ @@ -1519,6 +1520,13 @@ pub enum ClosureReason { }, } +#[cfg(feature = "uniffi")] +uniffi::custom_type!(NodeFeatures, Vec, { + remote, + try_lift: |val| Ok(NodeFeatures::from_le_bytes(val)), + lower: |obj| obj.le_flags().to_vec(), +}); + #[cfg(test)] mod tests { use std::num::NonZeroU64; diff --git a/src/lib.rs b/src/lib.rs index 6d877ae10d..25c14cf911 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,7 +151,8 @@ use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; pub use lightning::ln::channel_state::ChannelShutdownState; use lightning::ln::channelmanager::PaymentId; -use lightning::ln::msgs::SocketAddress; +use lightning::ln::msgs::{BaseMessageHandler, SocketAddress}; +use lightning::ln::peer_handler::CustomMessageHandler; use lightning::routing::gossip::NodeAlias; use lightning::sign::EntropySource; use lightning::util::persist::KVStoreSync; @@ -160,6 +161,7 @@ use lightning_background_processor::process_events_async; pub use lightning_invoice; pub use lightning_liquidity; pub use lightning_types; +use lightning_types::features::NodeFeatures; use liquidity::{LSPS1Liquidity, LiquiditySource}; use lnurl_auth::LnurlAuth; use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; @@ -775,6 +777,7 @@ impl Node { locked_node_metrics.latest_pathfinding_scores_sync_timestamp; let latest_node_announcement_broadcast_timestamp = locked_node_metrics.latest_node_announcement_broadcast_timestamp; + let node_features = self.node_features(); NodeStatus { is_running, @@ -786,6 +789,7 @@ impl Node { latest_rgs_snapshot_timestamp, latest_pathfinding_scores_sync_timestamp, latest_node_announcement_broadcast_timestamp, + node_features, } } @@ -2053,6 +2057,28 @@ impl Node { Error::PersistenceFailed }) } + + /// Return the features used in node announcement. + fn node_features(&self) -> NodeFeatures { + let gossip_features = match self.gossip_source.as_gossip_sync() { + lightning_background_processor::GossipSync::P2P(p2p_gossip_sync) => { + p2p_gossip_sync.provided_node_features() + }, + lightning_background_processor::GossipSync::Rapid(_) => NodeFeatures::empty(), + lightning_background_processor::GossipSync::None => { + unreachable!("We must always have a gossip sync!") + }, + }; + self.channel_manager.node_features() + | self.chain_monitor.provided_node_features() + | self.onion_messenger.provided_node_features() + | gossip_features + | self + .liquidity_source + .as_ref() + .map(|ls| ls.liquidity_manager().provided_node_features()) + .unwrap_or_else(NodeFeatures::empty) + } } impl Drop for Node { @@ -2114,6 +2140,8 @@ pub struct NodeStatus { /// /// Will be `None` if we have no public channels or we haven't broadcasted yet. pub latest_node_announcement_broadcast_timestamp: Option, + /// The features used within a node_announcement message. + pub node_features: NodeFeatures, } /// Status fields that are persisted across restarts. From 0668b4f840859ce03c9ab029b293ecbdf36cf0c0 Mon Sep 17 00:00:00 2001 From: Enigbe Date: Tue, 12 May 2026 22:25:52 +0100 Subject: [PATCH 2/2] fixup! Expose node announcement feature flags Switch the UniFFI NodeFeatures exposure from a raw custom Vec conversion to an object wrapper with BOLT 9 byte encoding helpers and typed feature accessors. --- src/ffi/types.rs | 302 +++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 18 ++- 2 files changed, 307 insertions(+), 13 deletions(-) diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 9e8886c647..43013f47b8 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -44,7 +44,7 @@ pub use lightning_liquidity::lsps0::ser::LSPSDateTime; pub use lightning_liquidity::lsps1::msgs::{ LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentState, }; -use lightning_types::features::NodeFeatures; +use lightning_types::features::NodeFeatures as LdkNodeFeatures; pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning_types::string::UntrustedString; use vss_client::headers::{ @@ -1520,12 +1520,300 @@ pub enum ClosureReason { }, } -#[cfg(feature = "uniffi")] -uniffi::custom_type!(NodeFeatures, Vec, { - remote, - try_lift: |val| Ok(NodeFeatures::from_le_bytes(val)), - lower: |obj| obj.le_flags().to_vec(), -}); +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)] +#[uniffi::export(Debug, Eq)] +pub struct NodeFeatures { + pub(crate) inner: LdkNodeFeatures, +} + +impl NodeFeatures { + /// Constructs node features from big-endian BOLT 9 encoded bytes. + #[uniffi::constructor] + pub fn from_bytes(bytes: &[u8]) -> Self { + Self { inner: LdkNodeFeatures::from_be_bytes(bytes.to_vec()) } + } + + /// Returns the BOLT 9 big-endian encoded representation of these features. + pub fn to_bytes(&self) -> Vec { + self.inner.encode() + } + + /// Whether this node's `node_announcement` advertises support for `option_data_loss_protect` (bit 1). + pub fn supports_data_loss_protect(&self) -> bool { + self.inner.supports_data_loss_protect() + } + + /// Whether this node's `node_announcement` requires `option_data_loss_protect` (bit 0). + pub fn requires_data_loss_protect(&self) -> bool { + self.inner.requires_data_loss_protect() + } + + /// Whether this node's `node_announcement` advertises support for `option_upfront_shutdown_script` (bit 5). + pub fn supports_upfront_shutdown_script(&self) -> bool { + self.inner.supports_upfront_shutdown_script() + } + + /// Whether this node's `node_announcement` requires `option_upfront_shutdown_script` (bit 4). + pub fn requires_upfront_shutdown_script(&self) -> bool { + self.inner.requires_upfront_shutdown_script() + } + + /// Whether this node's `node_announcement` advertises support for `gossip_queries` (bit 7). + pub fn supports_gossip_queries(&self) -> bool { + self.inner.supports_gossip_queries() + } + + /// Whether this node's `node_announcement` requires `gossip_queries` (bit 6). + pub fn requires_gossip_queries(&self) -> bool { + self.inner.requires_gossip_queries() + } + + /// Whether this node's `node_announcement` advertises support for `var_onion_optin` (bit 9). + pub fn supports_variable_length_onion(&self) -> bool { + self.inner.supports_variable_length_onion() + } + + /// Whether this node's `node_announcement` requires `var_onion_optin` (bit 8). + pub fn requires_variable_length_onion(&self) -> bool { + self.inner.requires_variable_length_onion() + } + + /// Whether this node's `node_announcement` advertises support for `option_static_remotekey` (bit 13). + pub fn supports_static_remote_key(&self) -> bool { + self.inner.supports_static_remote_key() + } + + /// Whether this node's `node_announcement` requires `option_static_remotekey` (bit 12). + pub fn requires_static_remote_key(&self) -> bool { + self.inner.requires_static_remote_key() + } + + /// Whether this node's `node_announcement` advertises support for `payment_secret` (bit 15). + pub fn supports_payment_secret(&self) -> bool { + self.inner.supports_payment_secret() + } + + /// Whether this node's `node_announcement` requires `payment_secret` (bit 14). + pub fn requires_payment_secret(&self) -> bool { + self.inner.requires_payment_secret() + } + + /// Whether this node's `node_announcement` advertises support for `basic_mpp` (bit 17). + pub fn supports_basic_mpp(&self) -> bool { + self.inner.supports_basic_mpp() + } + + /// Whether this node's `node_announcement` requires `basic_mpp` (bit 16). + pub fn requires_basic_mpp(&self) -> bool { + self.inner.requires_basic_mpp() + } + + /// Whether this node's `node_announcement` advertises support for `option_support_large_channel` (bit 19). + pub fn supports_wumbo(&self) -> bool { + self.inner.supports_wumbo() + } + + /// Whether this node's `node_announcement` requires `option_support_large_channel` (bit 18). + pub fn requires_wumbo(&self) -> bool { + self.inner.requires_wumbo() + } + + /// Whether this node's `node_announcement` advertises support for `option_anchors_nonzero_fee_htlc_tx` (bit 21). + pub fn supports_anchors_nonzero_fee_htlc_tx(&self) -> bool { + self.inner.supports_anchors_nonzero_fee_htlc_tx() + } + + /// Whether this node's `node_announcement` requires `option_anchors_nonzero_fee_htlc_tx` (bit 20). + pub fn requires_anchors_nonzero_fee_htlc_tx(&self) -> bool { + self.inner.requires_anchors_nonzero_fee_htlc_tx() + } + + /// Whether this node's `node_announcement` advertises support for `option_anchors_zero_fee_htlc_tx` (bit 23). + pub fn supports_anchors_zero_fee_htlc_tx(&self) -> bool { + self.inner.supports_anchors_zero_fee_htlc_tx() + } + + /// Whether this node's `node_announcement` requires `option_anchors_zero_fee_htlc_tx` (bit 22). + pub fn requires_anchors_zero_fee_htlc_tx(&self) -> bool { + self.inner.requires_anchors_zero_fee_htlc_tx() + } + + /// Whether this node's `node_announcement` advertises support for `option_route_blinding` (bit 25). + pub fn supports_route_blinding(&self) -> bool { + self.inner.supports_route_blinding() + } + + /// Whether this node's `node_announcement` requires `option_route_blinding` (bit 24). + pub fn requires_route_blinding(&self) -> bool { + self.inner.requires_route_blinding() + } + + /// Whether this node's `node_announcement` advertises support for `opt_shutdown_anysegwit` (bit 27). + pub fn supports_shutdown_anysegwit(&self) -> bool { + self.inner.supports_shutdown_anysegwit() + } + + /// Whether this node's `node_announcement` requires `opt_shutdown_anysegwit` (bit 26). + pub fn requires_shutdown_anysegwit(&self) -> bool { + self.inner.requires_shutdown_anysegwit() + } + + /// Whether this node's `node_announcement` advertises support for `option_dual_fund` (bit 29). + pub fn supports_dual_fund(&self) -> bool { + self.inner.supports_dual_fund() + } + + /// Whether this node's `node_announcement` requires `option_dual_fund` (bit 28). + pub fn requires_dual_fund(&self) -> bool { + self.inner.requires_dual_fund() + } + + /// Whether this node's `node_announcement` advertises support for `option_taproot` (bit 31). + pub fn supports_taproot(&self) -> bool { + self.inner.supports_taproot() + } + + /// Whether this node's `node_announcement` requires `option_taproot` (bit 30). + pub fn requires_taproot(&self) -> bool { + self.inner.requires_taproot() + } + + /// Whether this node's `node_announcement` advertises support for `option_quiesce` (bit 35). + pub fn supports_quiescence(&self) -> bool { + self.inner.supports_quiescence() + } + + /// Whether this node's `node_announcement` requires `option_quiesce` (bit 34). + pub fn requires_quiescence(&self) -> bool { + self.inner.requires_quiescence() + } + + /// Whether this node's `node_announcement` advertises support for `option_onion_messages` (bit 39). + pub fn supports_onion_messages(&self) -> bool { + self.inner.supports_onion_messages() + } + + /// Whether this node's `node_announcement` requires `option_onion_messages` (bit 38). + pub fn requires_onion_messages(&self) -> bool { + self.inner.requires_onion_messages() + } + + /// Whether this node's `node_announcement` advertises support for `option_provide_storage` (bit 43). + pub fn supports_provide_storage(&self) -> bool { + self.inner.supports_provide_storage() + } + + /// Whether this node's `node_announcement` requires `option_provide_storage` (bit 42). + pub fn requires_provide_storage(&self) -> bool { + self.inner.requires_provide_storage() + } + + /// Whether this node's `node_announcement` advertises support for `option_channel_type` (bit 45). + pub fn supports_channel_type(&self) -> bool { + self.inner.supports_channel_type() + } + + /// Whether this node's `node_announcement` requires `option_channel_type` (bit 44). + pub fn requires_channel_type(&self) -> bool { + self.inner.requires_channel_type() + } + + /// Whether this node's `node_announcement` advertises support for `option_scid_alias` (bit 47). + pub fn supports_scid_privacy(&self) -> bool { + self.inner.supports_scid_privacy() + } + + /// Whether this node's `node_announcement` requires `option_scid_alias` (bit 46). + pub fn requires_scid_privacy(&self) -> bool { + self.inner.requires_scid_privacy() + } + + /// Whether this node's `node_announcement` advertises support for `option_zeroconf` (bit 51). + pub fn supports_zero_conf(&self) -> bool { + self.inner.supports_zero_conf() + } + + /// Whether this node's `node_announcement` requires `option_zeroconf` (bit 50). + pub fn requires_zero_conf(&self) -> bool { + self.inner.requires_zero_conf() + } + + /// Whether this node's `node_announcement` advertises support for `keysend` (bit 55). + pub fn supports_keysend(&self) -> bool { + self.inner.supports_keysend() + } + + /// Whether this node's `node_announcement` requires `keysend` (bit 54). + pub fn requires_keysend(&self) -> bool { + self.inner.requires_keysend() + } + + /// Whether this node's `node_announcement` advertises support for `option_trampoline` (bit 57). + pub fn supports_trampoline_routing(&self) -> bool { + self.inner.supports_trampoline_routing() + } + + /// Whether this node's `node_announcement` requires `option_trampoline` (bit 56). + pub fn requires_trampoline_routing(&self) -> bool { + self.inner.requires_trampoline_routing() + } + + /// Whether this node's `node_announcement` advertises support for `option_simple_close` (bit 61). + pub fn supports_simple_close(&self) -> bool { + self.inner.supports_simple_close() + } + + /// Whether this node's `node_announcement` requires `option_simple_close` (bit 60). + pub fn requires_simple_close(&self) -> bool { + self.inner.requires_simple_close() + } + + /// Whether this node's `node_announcement` advertises support for `option_splice` (bit 63). + pub fn supports_splicing(&self) -> bool { + self.inner.supports_splicing() + } + + /// Whether this node's `node_announcement` requires `option_splice` (bit 62). + pub fn requires_splicing(&self) -> bool { + self.inner.requires_splicing() + } + + /// Whether this node's `node_announcement` advertises support for `option_zero_fee_commitments` (bit 141, experimental). + pub fn supports_anchor_zero_fee_commitments(&self) -> bool { + self.inner.supports_anchor_zero_fee_commitments() + } + + /// Whether this node's `node_announcement` requires `option_zero_fee_commitments` (bit 140, experimental). + pub fn requires_anchor_zero_fee_commitments(&self) -> bool { + self.inner.requires_anchor_zero_fee_commitments() + } + + /// Whether this node's `node_announcement` advertises support for HTLC hold (bit 153, experimental). + pub fn supports_htlc_hold(&self) -> bool { + self.inner.supports_htlc_hold() + } + + /// Whether this node's `node_announcement` requires HTLC hold (bit 152, experimental). + pub fn requires_htlc_hold(&self) -> bool { + self.inner.requires_htlc_hold() + } + + /// Whether this node's `node_announcement` advertises support for DNS resolution (bit 259). + pub fn supports_dns_resolution(&self) -> bool { + self.inner.supports_dns_resolution() + } + + /// Whether this node's `node_announcement` requires DNS resolution (bit 258). + pub fn requires_dns_resolution(&self) -> bool { + self.inner.requires_dns_resolution() + } +} + +impl From for NodeFeatures { + fn from(features: LdkNodeFeatures) -> Self { + Self { inner: features } + } +} #[cfg(test)] mod tests { diff --git a/src/lib.rs b/src/lib.rs index 25c14cf911..e5c9ce22a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; #[cfg(cycle_tests)] use std::{any::Any, sync::Weak}; +use crate::ffi::maybe_wrap; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; pub use bip39; pub use bitcoin; @@ -161,7 +162,7 @@ use lightning_background_processor::process_events_async; pub use lightning_invoice; pub use lightning_liquidity; pub use lightning_types; -use lightning_types::features::NodeFeatures; +use lightning_types::features::NodeFeatures as LdkNodeFeatures; use liquidity::{LSPS1Liquidity, LiquiditySource}; use lnurl_auth::LnurlAuth; use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; @@ -185,6 +186,11 @@ pub use vss_client; use crate::scoring::setup_background_pathfinding_scores_sync; use crate::wallet::FundingAmount; +#[cfg(not(feature = "uniffi"))] +type NodeFeatures = LdkNodeFeatures; +#[cfg(feature = "uniffi")] +type NodeFeatures = Arc; + #[cfg(feature = "uniffi")] uniffi::include_scaffolding!("ldk_node"); @@ -777,7 +783,7 @@ impl Node { locked_node_metrics.latest_pathfinding_scores_sync_timestamp; let latest_node_announcement_broadcast_timestamp = locked_node_metrics.latest_node_announcement_broadcast_timestamp; - let node_features = self.node_features(); + let node_features = maybe_wrap(self.node_features()); NodeStatus { is_running, @@ -2059,12 +2065,12 @@ impl Node { } /// Return the features used in node announcement. - fn node_features(&self) -> NodeFeatures { + fn node_features(&self) -> LdkNodeFeatures { let gossip_features = match self.gossip_source.as_gossip_sync() { lightning_background_processor::GossipSync::P2P(p2p_gossip_sync) => { p2p_gossip_sync.provided_node_features() }, - lightning_background_processor::GossipSync::Rapid(_) => NodeFeatures::empty(), + lightning_background_processor::GossipSync::Rapid(_) => LdkNodeFeatures::empty(), lightning_background_processor::GossipSync::None => { unreachable!("We must always have a gossip sync!") }, @@ -2077,7 +2083,7 @@ impl Node { .liquidity_source .as_ref() .map(|ls| ls.liquidity_manager().provided_node_features()) - .unwrap_or_else(NodeFeatures::empty) + .unwrap_or_else(LdkNodeFeatures::empty) } } @@ -2140,7 +2146,7 @@ pub struct NodeStatus { /// /// Will be `None` if we have no public channels or we haven't broadcasted yet. pub latest_node_announcement_broadcast_timestamp: Option, - /// The features used within a node_announcement message. + /// The features advertised in this node's `node_announcement` message. pub node_features: NodeFeatures, }