From 0c58f58a3c66294b888ebb2b583c0da98335023b Mon Sep 17 00:00:00 2001 From: Enigbe Date: Thu, 26 Feb 2026 13:41:08 +0100 Subject: [PATCH 1/2] Expose APIs to access supported feature flags With this, we can read the features supported by the node as announced in node and channel announcements, within an init message, and within a BOLT 11 invoice. We write a small integration test to assert that, at minimum, certain features are supported by default, for example, keysend support. --- src/lib.rs | 63 ++++++++++++++++++++++++++++++++- tests/integration_tests_rust.rs | 34 ++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 2e02e996c..00dbc8db3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,7 +151,8 @@ use lightning::impl_writeable_tlv_based; use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, 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,9 @@ use lightning_background_processor::process_events_async; pub use lightning_invoice; pub use lightning_liquidity; pub use lightning_types; +use lightning_types::features::{ + Bolt11InvoiceFeatures, ChannelFeatures, InitFeatures, NodeFeatures, +}; use liquidity::{LSPS1Liquidity, LiquiditySource}; use lnurl_auth::LnurlAuth; use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; @@ -1949,6 +1953,63 @@ impl Node { Error::PersistenceFailed }) } + + /// Return the features used in node announcement. + pub 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) + } + + /// Return the node's init features. + pub fn init_features(&self) -> InitFeatures { + let gossip_init_features = match self.gossip_source.as_gossip_sync() { + lightning_background_processor::GossipSync::P2P(p2p_gossip_sync) => { + p2p_gossip_sync.provided_init_features(self.node_id()) + }, + lightning_background_processor::GossipSync::Rapid(_) => InitFeatures::empty(), + lightning_background_processor::GossipSync::None => { + unreachable!("We must always have a gossip sync!") + }, + }; + self.channel_manager.init_features() + | self.chain_monitor.provided_init_features(self.node_id()) + | self.onion_messenger.provided_init_features(self.node_id()) + | gossip_init_features + | self + .liquidity_source + .as_ref() + .map(|ls| ls.liquidity_manager().provided_init_features(self.node_id())) + .unwrap_or_else(InitFeatures::empty) + } + + /// Return the node's channel features. + pub fn channel_features(&self) -> ChannelFeatures { + self.channel_manager.channel_features() + } + + /// Return the node's BOLT 11 invoice features. + pub fn bolt11_invoice_features(&self) -> Bolt11InvoiceFeatures { + // bolt11_invoice_features() is not public because feature + // flags can vary due to invoice type, so we convert from + // context. + self.channel_manager.init_features().to_context() + } } impl Drop for Node { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 3fde52dc4..5aa47ebe1 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -2805,3 +2805,37 @@ async fn splice_in_with_all_balance() { node_a.stop().unwrap(); node_b.stop().unwrap(); } + +#[test] +fn node_feature_flags() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let config = random_config(true); + let node = setup_node(&chain_source, config); + + // NodeFeatures + let node_features = node.node_features(); + assert!(node_features.supports_variable_length_onion()); + assert!(node_features.supports_payment_secret()); + assert!(node_features.supports_basic_mpp()); + assert!(node_features.supports_keysend()); + assert!(node_features.supports_onion_messages()); + + // InitFeatures + let init_features = node.init_features(); + assert!(init_features.supports_variable_length_onion()); + assert!(init_features.supports_payment_secret()); + assert!(init_features.supports_basic_mpp()); + assert!(init_features.supports_onion_messages()); + + // ChannelFeatures (non-empty) + let _channel_features = node.channel_features(); + + // Bolt11InvoiceFeatures + let bolt11_features = node.bolt11_invoice_features(); + assert!(bolt11_features.supports_variable_length_onion()); + assert!(bolt11_features.supports_payment_secret()); + assert!(bolt11_features.supports_basic_mpp()); + + node.stop().unwrap(); +} From 5a870419a92afc2127eba27f466bec3bb4adda81 Mon Sep 17 00:00:00 2001 From: Enigbe Date: Tue, 24 Mar 2026 21:55:42 +0100 Subject: [PATCH 2/2] fixup! Expose APIs to access supported feature flags This fixup moves node feature exposure from freestanding APIs to NodeStatus, as suggested in review. Rather than exposing init_features(), channel_features(), bolt11_invoice_features(), and node_features() as separate public methods on Node, this embeds NodeFeatures in the NodeStatus struct returned by Node::status(). Additionally, channel and invoice features at node level are confusing. Users would expect negotiated per-peer/channel/invoice features, not what the node generally supports. Access to negotiated features are addressed in #841 --- src/ffi/types.rs | 8 ++++++ src/lib.rs | 45 +++++---------------------------- tests/integration_tests_rust.rs | 34 ------------------------- 3 files changed, 14 insertions(+), 73 deletions(-) diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 5a1420882..39476996e 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -43,6 +43,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::{ @@ -1496,6 +1497,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 00dbc8db3..136e7c42e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -161,9 +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::{ - Bolt11InvoiceFeatures, ChannelFeatures, InitFeatures, NodeFeatures, -}; +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}; @@ -766,6 +764,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, @@ -776,6 +775,7 @@ impl Node { latest_rgs_snapshot_timestamp, latest_pathfinding_scores_sync_timestamp, latest_node_announcement_broadcast_timestamp, + node_features, } } @@ -1955,7 +1955,7 @@ impl Node { } /// Return the features used in node announcement. - pub fn node_features(&self) -> NodeFeatures { + 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() @@ -1975,41 +1975,6 @@ impl Node { .map(|ls| ls.liquidity_manager().provided_node_features()) .unwrap_or_else(NodeFeatures::empty) } - - /// Return the node's init features. - pub fn init_features(&self) -> InitFeatures { - let gossip_init_features = match self.gossip_source.as_gossip_sync() { - lightning_background_processor::GossipSync::P2P(p2p_gossip_sync) => { - p2p_gossip_sync.provided_init_features(self.node_id()) - }, - lightning_background_processor::GossipSync::Rapid(_) => InitFeatures::empty(), - lightning_background_processor::GossipSync::None => { - unreachable!("We must always have a gossip sync!") - }, - }; - self.channel_manager.init_features() - | self.chain_monitor.provided_init_features(self.node_id()) - | self.onion_messenger.provided_init_features(self.node_id()) - | gossip_init_features - | self - .liquidity_source - .as_ref() - .map(|ls| ls.liquidity_manager().provided_init_features(self.node_id())) - .unwrap_or_else(InitFeatures::empty) - } - - /// Return the node's channel features. - pub fn channel_features(&self) -> ChannelFeatures { - self.channel_manager.channel_features() - } - - /// Return the node's BOLT 11 invoice features. - pub fn bolt11_invoice_features(&self) -> Bolt11InvoiceFeatures { - // bolt11_invoice_features() is not public because feature - // flags can vary due to invoice type, so we convert from - // context. - self.channel_manager.init_features().to_context() - } } impl Drop for Node { @@ -2053,6 +2018,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. diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 5aa47ebe1..3fde52dc4 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -2805,37 +2805,3 @@ async fn splice_in_with_all_balance() { node_a.stop().unwrap(); node_b.stop().unwrap(); } - -#[test] -fn node_feature_flags() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let config = random_config(true); - let node = setup_node(&chain_source, config); - - // NodeFeatures - let node_features = node.node_features(); - assert!(node_features.supports_variable_length_onion()); - assert!(node_features.supports_payment_secret()); - assert!(node_features.supports_basic_mpp()); - assert!(node_features.supports_keysend()); - assert!(node_features.supports_onion_messages()); - - // InitFeatures - let init_features = node.init_features(); - assert!(init_features.supports_variable_length_onion()); - assert!(init_features.supports_payment_secret()); - assert!(init_features.supports_basic_mpp()); - assert!(init_features.supports_onion_messages()); - - // ChannelFeatures (non-empty) - let _channel_features = node.channel_features(); - - // Bolt11InvoiceFeatures - let bolt11_features = node.bolt11_invoice_features(); - assert!(bolt11_features.supports_variable_length_onion()); - assert!(bolt11_features.supports_payment_secret()); - assert!(bolt11_features.supports_basic_mpp()); - - node.stop().unwrap(); -}