Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions e2e-tests/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ async fn test_cli_get_node_info() {
let output = run_cli(&server, &["get-node-info"]);
assert!(output.get("node_id").is_some());
assert_eq!(output["node_id"], server.node_id());

// Ensure clients can inspect advertised node capabilities from get-node-info.
let keysend = &output["features"]["node"]["Keysend"];
assert_eq!(keysend["name"], "Keysend");
assert_eq!(keysend["is_supported"], true);
assert_eq!(keysend["is_required"], false);
assert_eq!(keysend["is_known"], true);
assert_eq!(keysend["supported_bit"], 55);
assert_eq!(keysend["required_bit"], 54);
}

#[tokio::test]
Expand Down Expand Up @@ -226,12 +235,14 @@ async fn test_cli_decode_invoice() {
feature_names
);

// Every entry should have the expected structure
for (bit, feature) in features {
assert!(bit.parse::<u32>().is_ok(), "Feature key should be a bit number: {}", bit);
// Every entry should have the expected structure.
for (name, feature) in features {
assert_eq!(feature["name"], *name);
assert!(feature.get("name").is_some(), "Feature missing name field");
assert!(feature.get("is_supported").is_some(), "Feature missing is_supported field");
assert!(feature.get("is_required").is_some(), "Feature missing is_required field");
assert!(feature.get("is_known").is_some(), "Feature missing is_known field");
assert!(feature.get("supported_bit").is_some(), "Feature missing supported_bit field");
}

// Also test a variable-amount invoice
Expand Down
18 changes: 12 additions & 6 deletions ldk-server-grpc/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ pub struct GetNodeInfoResponse {
#[prost(enumeration = "super::types::Network", tag = "13")]
#[cfg_attr(feature = "serde", serde(serialize_with = "crate::serde_utils::serialize_network"))]
pub network: i32,
/// The feature sets advertised by this node. Currently only node-announcement
/// features are populated.
#[prost(message, optional, tag = "14")]
pub features: ::core::option::Option<super::types::Features>,
}
/// Retrieve a new on-chain funding address.
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/payment/struct.OnchainPayment.html#method.new_address>
Expand Down Expand Up @@ -1164,9 +1168,10 @@ pub struct DecodeInvoiceResponse {
/// Route hints for finding a path to the payee.
#[prost(message, repeated, tag = "10")]
pub route_hints: ::prost::alloc::vec::Vec<super::types::Bolt11RouteHint>,
/// Feature bits advertised in the invoice, keyed by bit number.
#[prost(map = "uint32, message", tag = "11")]
pub features: ::std::collections::HashMap<u32, super::types::Bolt11Feature>,
/// Features advertised in the invoice, keyed by feature name.
#[prost(map = "string, message", tag = "11")]
pub features:
::std::collections::HashMap<::prost::alloc::string::String, super::types::Feature>,
/// The currency or network (e.g., "bitcoin", "testnet", "signet", "regtest").
#[prost(string, tag = "12")]
pub currency: ::prost::alloc::string::String,
Expand Down Expand Up @@ -1220,9 +1225,10 @@ pub struct DecodeOfferResponse {
/// Blinded paths to the offer recipient.
#[prost(message, repeated, tag = "8")]
pub paths: ::prost::alloc::vec::Vec<super::types::BlindedPath>,
/// Feature bits advertised in the offer, keyed by bit number.
#[prost(map = "uint32, message", tag = "9")]
pub features: ::std::collections::HashMap<u32, super::types::Bolt11Feature>,
/// Features advertised in the offer, keyed by feature name.
#[prost(map = "string, message", tag = "9")]
pub features:
::std::collections::HashMap<::prost::alloc::string::String, super::types::Feature>,
/// Supported blockchain networks (e.g., "bitcoin", "testnet", "signet", "regtest").
#[prost(string, repeated, tag = "10")]
pub chains: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
Expand Down
12 changes: 8 additions & 4 deletions ldk-server-grpc/src/proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ message GetNodeInfoResponse {

// The Bitcoin network the node is running on (e.g., "bitcoin", "testnet", "signet", "regtest").
types.Network network = 13;

// The feature sets advertised by this node. Currently only node-announcement
// features are populated.
types.Features features = 14;
}

// Retrieve a new on-chain funding address.
Expand Down Expand Up @@ -840,8 +844,8 @@ message DecodeInvoiceResponse {
// Route hints for finding a path to the payee.
repeated types.Bolt11RouteHint route_hints = 10;

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

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

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

// Supported blockchain networks (e.g., "bitcoin", "testnet", "signet", "regtest").
repeated string chains = 10;
Expand Down
30 changes: 24 additions & 6 deletions ldk-server-grpc/src/proto/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -924,16 +924,27 @@ enum ChannelDirection {
NODE_TWO = 1;
}

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

// Whether this feature is required.
bool is_required = 2;
// Whether this feature's support bit is set.
bool is_supported = 2;

// Whether this feature is known.
bool is_known = 3;
// Whether this feature's required bit is set.
bool is_required = 3;

// Whether this feature is known by LDK.
bool is_known = 4;

// The BOLT 9 bit that indicates support for this feature.
uint32 supported_bit = 5;

// The BOLT 9 bit that requires support for this feature, if one exists.
// Optional because some feature contexts include support-only features without
// a corresponding required bit, e.g. `initial_routing_sync` in init features.
optional uint32 required_bit = 6;
}

// Custom TLV record attached to a payment.
Expand All @@ -943,3 +954,10 @@ message CustomTlvRecord {
// Raw TLV value.
bytes value = 2;
}

// The feature sets advertised by this node. Currently only node-announcement
// features are populated.
message Features {
// Node-announcement features keyed by feature name.
map<string, Feature> node = 1;
}
33 changes: 28 additions & 5 deletions ldk-server-grpc/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1212,22 +1212,33 @@ pub struct DirectedShortChannelId {
)]
pub direction: i32,
}
/// A feature bit advertised in a BOLT11 invoice.
/// A feature advertised in a BOLT feature context.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[cfg_attr(feature = "serde", serde(default))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Bolt11Feature {
pub struct Feature {
/// Human-readable feature name.
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
/// Whether this feature is required.
/// Whether this feature's support bit is set.
#[prost(bool, tag = "2")]
pub is_required: bool,
/// Whether this feature is known.
pub is_supported: bool,
/// Whether this feature's required bit is set.
#[prost(bool, tag = "3")]
pub is_required: bool,
/// Whether this feature is known by LDK.
#[prost(bool, tag = "4")]
pub is_known: bool,
/// The BOLT 9 bit that indicates support for this feature.
#[prost(uint32, tag = "5")]
pub supported_bit: u32,
/// The BOLT 9 bit that requires support for this feature, if one exists.
/// Optional because some feature contexts include support-only features without
/// a corresponding required bit, e.g. `initial_routing_sync` in init features.
#[prost(uint32, optional, tag = "6")]
pub required_bit: ::core::option::Option<u32>,
}
/// Custom TLV record attached to a payment.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand All @@ -1243,6 +1254,18 @@ pub struct CustomTlvRecord {
#[prost(bytes = "bytes", tag = "2")]
pub value: ::prost::bytes::Bytes,
}
/// The feature sets advertised by this node. Currently only node-announcement
/// features are populated.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[cfg_attr(feature = "serde", serde(default))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Features {
/// Node-announcement features keyed by feature name.
#[prost(map = "string, message", tag = "1")]
pub node: ::std::collections::HashMap<::prost::alloc::string::String, Feature>,
}
/// Represents the direction of a payment.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
Expand Down
4 changes: 2 additions & 2 deletions ldk-server/src/api/decode_invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ use ldk_node::lightning_types::features::Bolt11InvoiceFeatures;
use ldk_server_grpc::api::{DecodeInvoiceRequest, DecodeInvoiceResponse};
use ldk_server_grpc::types::{Bolt11HopHint, Bolt11RouteHint};

use crate::api::decode_features;
use crate::api::error::LdkServerError;
use crate::service::Context;
use crate::util::proto_adapter::features_to_proto;

pub(crate) async fn handle_decode_invoice_request(
_context: Arc<Context>, request: DecodeInvoiceRequest,
Expand Down Expand Up @@ -66,7 +66,7 @@ pub(crate) async fn handle_decode_invoice_request(
let features = invoice
.features()
.map(|f| {
decode_features(f.le_flags(), |bytes| {
features_to_proto(f.le_flags(), |bytes| {
Bolt11InvoiceFeatures::from_le_bytes(bytes).to_string()
})
})
Expand Down
4 changes: 2 additions & 2 deletions ldk-server/src/api/decode_offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ use ldk_server_grpc::types::{
OfferQuantity,
};

use crate::api::decode_features;
use crate::api::error::LdkServerError;
use crate::service::Context;
use crate::util::proto_adapter::features_to_proto;

pub(crate) async fn handle_decode_offer_request(
_context: Arc<Context>, request: DecodeOfferRequest,
Expand Down Expand Up @@ -104,7 +104,7 @@ pub(crate) async fn handle_decode_offer_request(
})
.collect();

let features = decode_features(offer.offer_features().le_flags(), |bytes| {
let features = features_to_proto(offer.offer_features().le_flags(), |bytes| {
OfferFeatures::from_le_bytes(bytes).to_string()
});

Expand Down
12 changes: 10 additions & 2 deletions ldk-server/src/api/get_node_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@

use std::sync::Arc;

use ldk_node::lightning_types::features::NodeFeatures;
use ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoResponse};
use ldk_server_grpc::types::BestBlock;
use ldk_server_grpc::types::{BestBlock, Features};

use crate::api::error::LdkServerError;
use crate::service::Context;
use crate::util::proto_adapter::network_to_proto;
use crate::util::proto_adapter::{features_to_proto, network_to_proto};

pub(crate) async fn handle_get_node_info_request(
context: Arc<Context>, _request: GetNodeInfoRequest,
Expand All @@ -26,6 +27,12 @@ pub(crate) async fn handle_get_node_info_request(
height: node_status.current_best_block.height,
};

let features = Features {
node: features_to_proto(node_status.node_features.le_flags(), |bytes| {
NodeFeatures::from_le_bytes(bytes).to_string()
}),
};

let listening_addresses: Vec<String> = context
.node
.listening_addresses()
Expand Down Expand Up @@ -66,6 +73,7 @@ pub(crate) async fn handle_get_node_info_request(
node_alias,
node_uris,
network,
features: Some(features),
};
Ok(response)
}
58 changes: 1 addition & 57 deletions ldk-server/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
// You may not use this file except in accordance with one or both of these
// licenses.

use std::collections::HashMap;

use ldk_node::config::{ChannelConfig, MaxDustHTLCExposure};
use ldk_node::lightning::routing::router::RouteParametersConfig;
use ldk_node::CustomTlvRecord as NodeCustomTlvRecord;
use ldk_server_grpc::types::channel_config::MaxDustHtlcExposure;
use ldk_server_grpc::types::{Bolt11Feature, CustomTlvRecord as ProtoCustomTlvRecord};
use ldk_server_grpc::types::CustomTlvRecord as ProtoCustomTlvRecord;

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

/// Decodes feature flags into a map keyed by bit number. Feature names are derived
/// from LDK's `Features::Display` impl, so they stay in sync automatically.
///
/// `make_display` should construct a `Features<T>` from the given LE bytes and return
/// its `to_string()` output — this lets us probe LDK for the name of each set bit.
pub(crate) fn decode_features(
le_flags: &[u8], make_display: impl Fn(Vec<u8>) -> String,
) -> HashMap<u32, Bolt11Feature> {
let mut features = HashMap::new();
for (byte_idx, &byte) in le_flags.iter().enumerate() {
if byte == 0 {
continue;
}
for bit_pos in 0..8u32 {
if byte & (1 << bit_pos) != 0 {
let bit_number = (byte_idx as u32) * 8 + bit_pos;
let is_required = bit_number % 2 == 0;

// Create Features with just this bit set and use Display to get the name.
let mut single_bit = vec![0u8; byte_idx + 1];
single_bit[byte_idx] = 1 << bit_pos;
let display = make_display(single_bit);
let (name, is_known) = parse_feature_name(&display);

features.insert(
bit_number,
Bolt11Feature { name: name.to_string(), is_required, is_known },
);
}
}
}
features
}

/// Parse the Display output of a single-bit Features to find which feature is set.
///
/// LDK's Display format is: "Name: status, Name: status, ..., unknown flags: status"
/// where status is "required", "supported", or "not supported".
/// For a single-bit Features, exactly one entry will be "required" or "supported".
fn parse_feature_name(display: &str) -> (&str, bool) {
for entry in display.split(", ") {
if let Some((name, status)) = entry.split_once(": ") {
if name == "unknown flags" {
if status == "required" || status == "supported" {
return ("unknown", false);
}
} else if status == "required" || status == "supported" {
return (name, true);
}
}
}
("unknown", false)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading