Skip to content

Commit da2670e

Browse files
committed
fixup! Expose node features in get-node-info
Decode exposed features into semantic entries We replace raw feature bytes, as previously used in NodeFeatures, with decoded Feature entries so clients can inspect the feature name, support bit, required bit, and known-ness directly. Given the structure of LDK Features, which are different based on contexts, we create and use this shared Feature shape for invoice, offer, and node feature contexts, with the conversion logic centralized in proto_adapter (see features_to_proto). Additionally, we also update the CLI e2e assertions to check keysend support through the readable get-node-info feature response.
1 parent 4222171 commit da2670e

10 files changed

Lines changed: 156 additions & 106 deletions

File tree

e2e-tests/tests/e2e.rs

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ use hex_conservative::{DisplayHex, FromHex};
2020
use ldk_node::bitcoin::hashes::{sha256, Hash};
2121
use ldk_node::lightning::ln::msgs::SocketAddress;
2222
use ldk_node::lightning::offers::offer::Offer;
23-
use ldk_node::lightning::util::ser::Readable;
24-
use ldk_node::lightning_types::features::NodeFeatures;
2523
use ldk_node::lightning_invoice::Bolt11Invoice;
2624
use ldk_server_client::client::EventStream;
2725
use ldk_server_client::ldk_server_grpc::api::{
@@ -59,16 +57,14 @@ async fn test_cli_get_node_info() {
5957
assert!(output.get("node_id").is_some());
6058
assert_eq!(output["node_id"], server.node_id());
6159

62-
// Ensure clients can decode advertised node capabilities from get-node-info.
63-
let node_feature_bytes: Vec<u8> = output["features"]["node"]
64-
.as_array()
65-
.unwrap()
66-
.iter()
67-
.map(|byte| byte.as_u64().unwrap() as u8)
68-
.collect();
69-
let mut node_feature_bytes = node_feature_bytes.as_slice();
70-
let node_features = NodeFeatures::read(&mut node_feature_bytes).unwrap();
71-
assert!(node_features.supports_keysend());
60+
// Ensure clients can inspect advertised node capabilities from get-node-info.
61+
let keysend = &output["features"]["node"]["Keysend"];
62+
assert_eq!(keysend["name"], "Keysend");
63+
assert_eq!(keysend["is_supported"], true);
64+
assert_eq!(keysend["is_required"], false);
65+
assert_eq!(keysend["is_known"], true);
66+
assert_eq!(keysend["supported_bit"], 55);
67+
assert_eq!(keysend["required_bit"], 54);
7268
}
7369

7470
#[tokio::test]
@@ -239,12 +235,14 @@ async fn test_cli_decode_invoice() {
239235
feature_names
240236
);
241237

242-
// Every entry should have the expected structure
243-
for (bit, feature) in features {
244-
assert!(bit.parse::<u32>().is_ok(), "Feature key should be a bit number: {}", bit);
238+
// Every entry should have the expected structure.
239+
for (name, feature) in features {
240+
assert_eq!(feature["name"], *name);
245241
assert!(feature.get("name").is_some(), "Feature missing name field");
242+
assert!(feature.get("is_supported").is_some(), "Feature missing is_supported field");
246243
assert!(feature.get("is_required").is_some(), "Feature missing is_required field");
247244
assert!(feature.get("is_known").is_some(), "Feature missing is_known field");
245+
assert!(feature.get("supported_bit").is_some(), "Feature missing supported_bit field");
248246
}
249247

250248
// Also test a variable-amount invoice

ldk-server-grpc/src/api.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,9 +1168,10 @@ pub struct DecodeInvoiceResponse {
11681168
/// Route hints for finding a path to the payee.
11691169
#[prost(message, repeated, tag = "10")]
11701170
pub route_hints: ::prost::alloc::vec::Vec<super::types::Bolt11RouteHint>,
1171-
/// Feature bits advertised in the invoice, keyed by bit number.
1172-
#[prost(map = "uint32, message", tag = "11")]
1173-
pub features: ::std::collections::HashMap<u32, super::types::Bolt11Feature>,
1171+
/// Features advertised in the invoice, keyed by feature name.
1172+
#[prost(map = "string, message", tag = "11")]
1173+
pub features:
1174+
::std::collections::HashMap<::prost::alloc::string::String, super::types::Feature>,
11741175
/// The currency or network (e.g., "bitcoin", "testnet", "signet", "regtest").
11751176
#[prost(string, tag = "12")]
11761177
pub currency: ::prost::alloc::string::String,
@@ -1224,9 +1225,10 @@ pub struct DecodeOfferResponse {
12241225
/// Blinded paths to the offer recipient.
12251226
#[prost(message, repeated, tag = "8")]
12261227
pub paths: ::prost::alloc::vec::Vec<super::types::BlindedPath>,
1227-
/// Feature bits advertised in the offer, keyed by bit number.
1228-
#[prost(map = "uint32, message", tag = "9")]
1229-
pub features: ::std::collections::HashMap<u32, super::types::Bolt11Feature>,
1228+
/// Features advertised in the offer, keyed by feature name.
1229+
#[prost(map = "string, message", tag = "9")]
1230+
pub features:
1231+
::std::collections::HashMap<::prost::alloc::string::String, super::types::Feature>,
12301232
/// Supported blockchain networks (e.g., "bitcoin", "testnet", "signet", "regtest").
12311233
#[prost(string, repeated, tag = "10")]
12321234
pub chains: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -844,8 +844,8 @@ message DecodeInvoiceResponse {
844844
// Route hints for finding a path to the payee.
845845
repeated types.Bolt11RouteHint route_hints = 10;
846846

847-
// Feature bits advertised in the invoice, keyed by bit number.
848-
map<uint32, types.Bolt11Feature> features = 11;
847+
// Features advertised in the invoice, keyed by feature name.
848+
map<string, types.Feature> features = 11;
849849

850850
// The currency or network (e.g., "bitcoin", "testnet", "signet", "regtest").
851851
string currency = 12;
@@ -890,8 +890,8 @@ message DecodeOfferResponse {
890890
// Blinded paths to the offer recipient.
891891
repeated types.BlindedPath paths = 8;
892892

893-
// Feature bits advertised in the offer, keyed by bit number.
894-
map<uint32, types.Bolt11Feature> features = 9;
893+
// Features advertised in the offer, keyed by feature name.
894+
map<string, types.Feature> features = 9;
895895

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

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -924,16 +924,27 @@ 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.
933-
bool is_required = 2;
932+
// Whether this feature's support bit is set.
933+
bool is_supported = 2;
934934

935-
// Whether this feature is known.
936-
bool is_known = 3;
935+
// Whether this feature's required bit is set.
936+
bool is_required = 3;
937+
938+
// Whether this feature is known by LDK.
939+
bool is_known = 4;
940+
941+
// The BOLT 9 bit that indicates support for this feature.
942+
uint32 supported_bit = 5;
943+
944+
// The BOLT 9 bit that requires support for this feature, if one exists.
945+
// Optional because some feature contexts include support-only features without
946+
// a corresponding required bit, e.g. `initial_routing_sync` in init features.
947+
optional uint32 required_bit = 6;
937948
}
938949

939950
// Custom TLV record attached to a payment.
@@ -947,6 +958,6 @@ message CustomTlvRecord {
947958
// The feature sets advertised by this node. Currently only node-announcement
948959
// features are populated.
949960
message Features {
950-
// Serialized node-announcement features.
951-
bytes node = 1;
961+
// Node-announcement features keyed by feature name.
962+
map<string, Feature> node = 1;
952963
}

ldk-server-grpc/src/types.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,22 +1212,33 @@ 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 this feature's support bit is set.
12261226
#[prost(bool, tag = "2")]
1227-
pub is_required: bool,
1228-
/// Whether this feature is known.
1227+
pub is_supported: bool,
1228+
/// Whether this feature's required bit is set.
12291229
#[prost(bool, tag = "3")]
1230+
pub is_required: bool,
1231+
/// Whether this feature is known by LDK.
1232+
#[prost(bool, tag = "4")]
12301233
pub is_known: bool,
1234+
/// The BOLT 9 bit that indicates support for this feature.
1235+
#[prost(uint32, tag = "5")]
1236+
pub supported_bit: u32,
1237+
/// The BOLT 9 bit that requires support for this feature, if one exists.
1238+
/// Optional because some feature contexts include support-only features without
1239+
/// a corresponding required bit, e.g. `initial_routing_sync` in init features.
1240+
#[prost(uint32, optional, tag = "6")]
1241+
pub required_bit: ::core::option::Option<u32>,
12311242
}
12321243
/// Custom TLV record attached to a payment.
12331244
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -1251,9 +1262,9 @@ pub struct CustomTlvRecord {
12511262
#[allow(clippy::derive_partial_eq_without_eq)]
12521263
#[derive(Clone, PartialEq, ::prost::Message)]
12531264
pub struct Features {
1254-
/// Serialized node-announcement features.
1255-
#[prost(bytes = "bytes", tag = "1")]
1256-
pub node: ::prost::bytes::Bytes,
1265+
/// Node-announcement features keyed by feature name.
1266+
#[prost(map = "string, message", tag = "1")]
1267+
pub node: ::std::collections::HashMap<::prost::alloc::string::String, Feature>,
12571268
}
12581269
/// Represents the direction of a payment.
12591270
#[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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99

1010
use std::sync::Arc;
1111

12-
use ldk_node::lightning::util::ser::Writeable;
12+
use ldk_node::lightning_types::features::NodeFeatures;
1313
use ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoResponse};
1414
use ldk_server_grpc::types::{BestBlock, Features};
1515

1616
use crate::api::error::LdkServerError;
1717
use crate::service::Context;
18-
use crate::util::proto_adapter::network_to_proto;
18+
use crate::util::proto_adapter::{features_to_proto, network_to_proto};
1919

2020
pub(crate) async fn handle_get_node_info_request(
2121
context: Arc<Context>, _request: GetNodeInfoRequest,
@@ -27,7 +27,11 @@ pub(crate) async fn handle_get_node_info_request(
2727
height: node_status.current_best_block.height,
2828
};
2929

30-
let features = Features { node: node_status.node_features.encode().into() };
30+
let features = Features {
31+
node: features_to_proto(node_status.node_features.le_flags(), |bytes| {
32+
NodeFeatures::from_le_bytes(bytes).to_string()
33+
}),
34+
};
3135

3236
let listening_addresses: Vec<String> = context
3337
.node

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)