Skip to content

Commit 45c6516

Browse files
committed
Expose node features in get-node-info
Return the node-announcement feature set from GetNodeInfoResponse so clients can inspect advertised node capabilities, such as keysend support, directly from the node info API. Decode exposed feature bytes into semantic entries keyed by the signaled BOLT feature bit. Each entry carries the decoded name and whether that bit is required, while presence in the map indicates the bit is signaled.
1 parent c8424db commit 45c6516

11 files changed

Lines changed: 129 additions & 115 deletions

File tree

e2e-tests/tests/e2e.rs

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ async fn test_cli_get_node_info() {
5656
let output = run_cli(&server, &["get-node-info"]);
5757
assert!(output.get("node_id").is_some());
5858
assert_eq!(output["node_id"], server.node_id());
59+
60+
// Ensure clients can inspect advertised node capabilities from get-node-info.
61+
let keysend = &output["features"]["55"];
62+
assert_eq!(keysend["name"], "Keysend");
63+
assert_eq!(keysend["is_required"], false);
5964
}
6065

6166
#[tokio::test]
@@ -207,33 +212,27 @@ async fn test_cli_decode_invoice() {
207212
// Verify features — LDK BOLT11 invoices always set VariableLengthOnion, PaymentSecret,
208213
// and BasicMPP.
209214
let features = decoded["features"].as_object().unwrap();
210-
assert!(!features.is_empty(), "Expected at least one feature");
211-
212-
let feature_names: Vec<&str> = features.values().filter_map(|f| f["name"].as_str()).collect();
213-
assert!(
214-
feature_names.contains(&"VariableLengthOnion"),
215-
"Expected VariableLengthOnion in features: {:?}",
216-
feature_names
217-
);
218-
assert!(
219-
feature_names.contains(&"PaymentSecret"),
220-
"Expected PaymentSecret in features: {:?}",
221-
feature_names
222-
);
223-
assert!(
224-
feature_names.contains(&"BasicMPP"),
225-
"Expected BasicMPP in features: {:?}",
226-
feature_names
227-
);
228215

229-
// Every entry should have the expected structure
216+
// Every entry should be keyed by the signaled bit and expose the decoded name
217+
// plus whether that bit is required.
230218
for (bit, feature) in features {
231-
assert!(bit.parse::<u32>().is_ok(), "Feature key should be a bit number: {}", bit);
219+
assert!(bit.parse::<u32>().is_ok(), "Feature key is not a bit number: {bit}");
232220
assert!(feature.get("name").is_some(), "Feature missing name field");
233221
assert!(feature.get("is_required").is_some(), "Feature missing is_required field");
234-
assert!(feature.get("is_known").is_some(), "Feature missing is_known field");
235222
}
236223

224+
let variable_length_onion = &features["8"];
225+
assert_eq!(variable_length_onion["name"], "VariableLengthOnion");
226+
assert_eq!(variable_length_onion["is_required"], true);
227+
228+
let payment_secret = &features["14"];
229+
assert_eq!(payment_secret["name"], "PaymentSecret");
230+
assert_eq!(payment_secret["is_required"], true);
231+
232+
let basic_mpp = &features["17"];
233+
assert_eq!(basic_mpp["name"], "BasicMPP");
234+
assert_eq!(basic_mpp["is_required"], false);
235+
237236
// Also test a variable-amount invoice
238237
let output_var = run_cli(&server, &["bolt11-receive", "-d", "no amount"]);
239238
let decoded_var =
@@ -927,17 +926,13 @@ async fn test_cli_spontaneous_send_with_custom_tlvs() {
927926
assert!(!output["payment_id"].as_str().unwrap().is_empty());
928927

929928
// The receiver must observe both TLVs in PaymentReceived.
930-
let event_b =
931-
wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentReceived(_))).await;
929+
let event_b = wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentReceived(_))).await;
932930
let Some(Event::PaymentReceived(pr)) = event_b.event else {
933931
panic!("expected PaymentReceived");
934932
};
935933
assert_eq!(pr.custom_records.len(), 2);
936-
let by_type: HashMap<u64, Vec<u8>> = pr
937-
.custom_records
938-
.into_iter()
939-
.map(|r| (r.type_num, r.value.to_vec()))
940-
.collect();
934+
let by_type: HashMap<u64, Vec<u8>> =
935+
pr.custom_records.into_iter().map(|r| (r.type_num, r.value.to_vec())).collect();
941936
assert_eq!(by_type.get(&65537).cloned(), Some(vec![0xde, 0xad, 0xbe, 0xef]));
942937
assert_eq!(by_type.get(&65539).cloned(), Some(vec![0xca, 0xfe]));
943938
}
@@ -1177,8 +1172,7 @@ async fn test_forwarded_payment_event() {
11771172
builder_c.set_liquidity_source_lsps2(b_node_id, b_addr, None);
11781173

11791174
let mnemonic_c = ldk_node::entropy::generate_entropy_mnemonic(None);
1180-
let node_entropy_c =
1181-
ldk_node::entropy::NodeEntropy::from_bip39_mnemonic(mnemonic_c, None);
1175+
let node_entropy_c = ldk_node::entropy::NodeEntropy::from_bip39_mnemonic(mnemonic_c, None);
11821176
let node_c = builder_c.build(node_entropy_c).unwrap();
11831177

11841178
node_c.start().unwrap();

ldk-server-grpc/build.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ fn main() {
3636
fn generate_protos() {
3737
prost_build::Config::new()
3838
.bytes(&["."])
39+
.btree_map(&[
40+
"api.GetNodeInfoResponse.features",
41+
"api.DecodeInvoiceResponse.features",
42+
"api.DecodeOfferResponse.features",
43+
])
3944
.type_attribute(
4045
".",
4146
"#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]",

ldk-server-grpc/src/api.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ pub struct GetNodeInfoResponse {
8585
#[prost(enumeration = "super::types::Network", tag = "13")]
8686
#[cfg_attr(feature = "serde", serde(serialize_with = "crate::serde_utils::serialize_network"))]
8787
pub network: i32,
88+
/// Features advertised by this node, keyed by the signaled BOLT feature bit.
89+
#[prost(btree_map = "uint32, message", tag = "14")]
90+
pub features: ::prost::alloc::collections::BTreeMap<u32, super::types::Feature>,
8891
}
8992
/// Retrieve a new on-chain funding address.
9093
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.new_address>
@@ -1164,9 +1167,9 @@ pub struct DecodeInvoiceResponse {
11641167
/// Route hints for finding a path to the payee.
11651168
#[prost(message, repeated, tag = "10")]
11661169
pub route_hints: ::prost::alloc::vec::Vec<super::types::Bolt11RouteHint>,
1167-
/// Feature bits advertised in the invoice, keyed by bit number.
1168-
#[prost(map = "uint32, message", tag = "11")]
1169-
pub features: ::std::collections::HashMap<u32, super::types::Bolt11Feature>,
1170+
/// Features advertised in the invoice, keyed by the signaled BOLT feature bit.
1171+
#[prost(btree_map = "uint32, message", tag = "11")]
1172+
pub features: ::prost::alloc::collections::BTreeMap<u32, super::types::Feature>,
11701173
/// The currency or network (e.g., "bitcoin", "testnet", "signet", "regtest").
11711174
#[prost(string, tag = "12")]
11721175
pub currency: ::prost::alloc::string::String,
@@ -1220,9 +1223,9 @@ pub struct DecodeOfferResponse {
12201223
/// Blinded paths to the offer recipient.
12211224
#[prost(message, repeated, tag = "8")]
12221225
pub paths: ::prost::alloc::vec::Vec<super::types::BlindedPath>,
1223-
/// Feature bits advertised in the offer, keyed by bit number.
1224-
#[prost(map = "uint32, message", tag = "9")]
1225-
pub features: ::std::collections::HashMap<u32, super::types::Bolt11Feature>,
1226+
/// Features advertised in the offer, keyed by the signaled BOLT feature bit.
1227+
#[prost(btree_map = "uint32, message", tag = "9")]
1228+
pub features: ::prost::alloc::collections::BTreeMap<u32, super::types::Feature>,
12261229
/// Supported blockchain networks (e.g., "bitcoin", "testnet", "signet", "regtest").
12271230
#[prost(string, repeated, tag = "10")]
12281231
pub chains: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,

ldk-server-grpc/src/proto/api.proto

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ message GetNodeInfoResponse {
7373

7474
// The Bitcoin network the node is running on (e.g., "bitcoin", "testnet", "signet", "regtest").
7575
types.Network network = 13;
76+
77+
// Features advertised by this node, keyed by the signaled BOLT feature bit.
78+
map<uint32, types.Feature> features = 14;
7679
}
7780

7881
// Retrieve a new on-chain funding address.
@@ -840,8 +843,8 @@ message DecodeInvoiceResponse {
840843
// Route hints for finding a path to the payee.
841844
repeated types.Bolt11RouteHint route_hints = 10;
842845

843-
// Feature bits advertised in the invoice, keyed by bit number.
844-
map<uint32, types.Bolt11Feature> features = 11;
846+
// Features advertised in the invoice, keyed by the signaled BOLT feature bit.
847+
map<uint32, types.Feature> features = 11;
845848

846849
// The currency or network (e.g., "bitcoin", "testnet", "signet", "regtest").
847850
string currency = 12;
@@ -886,8 +889,8 @@ message DecodeOfferResponse {
886889
// Blinded paths to the offer recipient.
887890
repeated types.BlindedPath paths = 8;
888891

889-
// Feature bits advertised in the offer, keyed by bit number.
890-
map<uint32, types.Bolt11Feature> features = 9;
892+
// Features advertised in the offer, keyed by the signaled BOLT feature bit.
893+
map<uint32, types.Feature> features = 9;
891894

892895
// Supported blockchain networks (e.g., "bitcoin", "testnet", "signet", "regtest").
893896
repeated string chains = 10;

ldk-server-grpc/src/proto/types.proto

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -924,16 +924,13 @@ enum ChannelDirection {
924924
NODE_TWO = 1;
925925
}
926926

927-
// A feature bit advertised in a BOLT11 invoice.
928-
message Bolt11Feature {
927+
// A feature advertised in a BOLT feature context.
928+
message Feature {
929929
// Human-readable feature name.
930930
string name = 1;
931931

932-
// Whether this feature is required.
932+
// Whether the signaled feature bit is required.
933933
bool is_required = 2;
934-
935-
// Whether this feature is known.
936-
bool is_known = 3;
937934
}
938935

939936
// Custom TLV record attached to a payment.

ldk-server-grpc/src/types.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,22 +1212,19 @@ pub struct DirectedShortChannelId {
12121212
)]
12131213
pub direction: i32,
12141214
}
1215-
/// A feature bit advertised in a BOLT11 invoice.
1215+
/// A feature advertised in a BOLT feature context.
12161216
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12171217
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
12181218
#[cfg_attr(feature = "serde", serde(default))]
12191219
#[allow(clippy::derive_partial_eq_without_eq)]
12201220
#[derive(Clone, PartialEq, ::prost::Message)]
1221-
pub struct Bolt11Feature {
1221+
pub struct Feature {
12221222
/// Human-readable feature name.
12231223
#[prost(string, tag = "1")]
12241224
pub name: ::prost::alloc::string::String,
1225-
/// Whether this feature is required.
1225+
/// Whether the signaled feature bit is required.
12261226
#[prost(bool, tag = "2")]
12271227
pub is_required: bool,
1228-
/// Whether this feature is known.
1229-
#[prost(bool, tag = "3")]
1230-
pub is_known: bool,
12311228
}
12321229
/// Custom TLV record attached to a payment.
12331230
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

ldk-server/src/api/decode_invoice.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ use ldk_node::lightning_types::features::Bolt11InvoiceFeatures;
1616
use ldk_server_grpc::api::{DecodeInvoiceRequest, DecodeInvoiceResponse};
1717
use ldk_server_grpc::types::{Bolt11HopHint, Bolt11RouteHint};
1818

19-
use crate::api::decode_features;
2019
use crate::api::error::LdkServerError;
2120
use crate::service::Context;
21+
use crate::util::proto_adapter::features_to_proto;
2222

2323
pub(crate) async fn handle_decode_invoice_request(
2424
_context: Arc<Context>, request: DecodeInvoiceRequest,
@@ -66,7 +66,7 @@ pub(crate) async fn handle_decode_invoice_request(
6666
let features = invoice
6767
.features()
6868
.map(|f| {
69-
decode_features(f.le_flags(), |bytes| {
69+
features_to_proto(f.le_flags(), |bytes| {
7070
Bolt11InvoiceFeatures::from_le_bytes(bytes).to_string()
7171
})
7272
})

ldk-server/src/api/decode_offer.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ use ldk_server_grpc::types::{
2424
OfferQuantity,
2525
};
2626

27-
use crate::api::decode_features;
2827
use crate::api::error::LdkServerError;
2928
use crate::service::Context;
29+
use crate::util::proto_adapter::features_to_proto;
3030

3131
pub(crate) async fn handle_decode_offer_request(
3232
_context: Arc<Context>, request: DecodeOfferRequest,
@@ -104,7 +104,7 @@ pub(crate) async fn handle_decode_offer_request(
104104
})
105105
.collect();
106106

107-
let features = decode_features(offer.offer_features().le_flags(), |bytes| {
107+
let features = features_to_proto(offer.offer_features().le_flags(), |bytes| {
108108
OfferFeatures::from_le_bytes(bytes).to_string()
109109
});
110110

ldk-server/src/api/get_node_info.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99

1010
use std::sync::Arc;
1111

12+
use ldk_node::lightning_types::features::NodeFeatures;
1213
use ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoResponse};
1314
use ldk_server_grpc::types::BestBlock;
1415

1516
use crate::api::error::LdkServerError;
1617
use crate::service::Context;
17-
use crate::util::proto_adapter::network_to_proto;
18+
use crate::util::proto_adapter::{features_to_proto, network_to_proto};
1819

1920
pub(crate) async fn handle_get_node_info_request(
2021
context: Arc<Context>, _request: GetNodeInfoRequest,
@@ -26,6 +27,10 @@ pub(crate) async fn handle_get_node_info_request(
2627
height: node_status.current_best_block.height,
2728
};
2829

30+
let features = features_to_proto(node_status.node_features.le_flags(), |bytes| {
31+
NodeFeatures::from_le_bytes(bytes).to_string()
32+
});
33+
2934
let listening_addresses: Vec<String> = context
3035
.node
3136
.listening_addresses()
@@ -66,6 +71,7 @@ pub(crate) async fn handle_get_node_info_request(
6671
node_alias,
6772
node_uris,
6873
network,
74+
features,
6975
};
7076
Ok(response)
7177
}

ldk-server/src/api/mod.rs

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@
77
// You may not use this file except in accordance with one or both of these
88
// licenses.
99

10-
use std::collections::HashMap;
11-
1210
use ldk_node::config::{ChannelConfig, MaxDustHTLCExposure};
1311
use ldk_node::lightning::routing::router::RouteParametersConfig;
1412
use ldk_node::CustomTlvRecord as NodeCustomTlvRecord;
1513
use ldk_server_grpc::types::channel_config::MaxDustHtlcExposure;
16-
use ldk_server_grpc::types::{Bolt11Feature, CustomTlvRecord as ProtoCustomTlvRecord};
14+
use ldk_server_grpc::types::CustomTlvRecord as ProtoCustomTlvRecord;
1715

1816
use crate::api::error::LdkServerError;
1917
use crate::api::error::LdkServerErrorCode::InvalidRequestError;
@@ -138,60 +136,6 @@ pub(crate) fn node_to_proto_custom_tlv(node: &NodeCustomTlvRecord) -> ProtoCusto
138136
ProtoCustomTlvRecord { type_num: node.type_num, value: node.value.clone().into() }
139137
}
140138

141-
/// Decodes feature flags into a map keyed by bit number. Feature names are derived
142-
/// from LDK's `Features::Display` impl, so they stay in sync automatically.
143-
///
144-
/// `make_display` should construct a `Features<T>` from the given LE bytes and return
145-
/// its `to_string()` output — this lets us probe LDK for the name of each set bit.
146-
pub(crate) fn decode_features(
147-
le_flags: &[u8], make_display: impl Fn(Vec<u8>) -> String,
148-
) -> HashMap<u32, Bolt11Feature> {
149-
let mut features = HashMap::new();
150-
for (byte_idx, &byte) in le_flags.iter().enumerate() {
151-
if byte == 0 {
152-
continue;
153-
}
154-
for bit_pos in 0..8u32 {
155-
if byte & (1 << bit_pos) != 0 {
156-
let bit_number = (byte_idx as u32) * 8 + bit_pos;
157-
let is_required = bit_number % 2 == 0;
158-
159-
// Create Features with just this bit set and use Display to get the name.
160-
let mut single_bit = vec![0u8; byte_idx + 1];
161-
single_bit[byte_idx] = 1 << bit_pos;
162-
let display = make_display(single_bit);
163-
let (name, is_known) = parse_feature_name(&display);
164-
165-
features.insert(
166-
bit_number,
167-
Bolt11Feature { name: name.to_string(), is_required, is_known },
168-
);
169-
}
170-
}
171-
}
172-
features
173-
}
174-
175-
/// Parse the Display output of a single-bit Features to find which feature is set.
176-
///
177-
/// LDK's Display format is: "Name: status, Name: status, ..., unknown flags: status"
178-
/// where status is "required", "supported", or "not supported".
179-
/// For a single-bit Features, exactly one entry will be "required" or "supported".
180-
fn parse_feature_name(display: &str) -> (&str, bool) {
181-
for entry in display.split(", ") {
182-
if let Some((name, status)) = entry.split_once(": ") {
183-
if name == "unknown flags" {
184-
if status == "required" || status == "supported" {
185-
return ("unknown", false);
186-
}
187-
} else if status == "required" || status == "supported" {
188-
return (name, true);
189-
}
190-
}
191-
}
192-
("unknown", false)
193-
}
194-
195139
#[cfg(test)]
196140
mod tests {
197141
use super::*;

0 commit comments

Comments
 (0)