From d39dc9d2b7573ec6a6a022a1113c6a4a8606275f Mon Sep 17 00:00:00 2001 From: ajaysehwal Date: Wed, 4 Nov 2026 16:27:57 +0530 Subject: [PATCH] Apply the default routing fee limit to spontaneous sends The documented default for `RouteParametersConfig::max_total_routing_fee_msat` is 1% of the payment amount plus 50 sats. When no route parameters are given at all, ldk-node keeps LDK's default limit. But ldk-node's spontaneous send applies provided route parameters as a whole, i.e., it overwrites the limit with the given value, which is `None` if the field is unset. A `None` limit means no limit, so a spontaneous send with route parameters but without an explicit fee limit could pay arbitrary routing fees. This affects every client that sends route parameters without a fee limit. Notably, `ldk-server-cli spontaneous-send` always sends route parameters (filling the other fields with their defaults), so it didn't limit routing fees unless `--max-total-routing-fee` was passed. BOLT11 and (non-async) BOLT12 payments are not affected, as LDK only overrides the default limit if one is given. Here, we fill in the documented default (1% of the amount plus 50 sats) in the spontaneous send handler whenever route parameters are provided without a fee limit. Explicit limits, including 0, are left untouched. --- ldk-server/src/api/mod.rs | 52 ++++++++++++++++++++++++++ ldk-server/src/api/spontaneous_send.rs | 10 ++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/ldk-server/src/api/mod.rs b/ldk-server/src/api/mod.rs index 04a92c87..f2abc4b9 100644 --- a/ldk-server/src/api/mod.rs +++ b/ldk-server/src/api/mod.rs @@ -170,6 +170,17 @@ pub(crate) fn build_route_parameters_config_from_proto( } } +pub(crate) fn with_default_max_routing_fee( + route_parameters: Option, amount_msat: u64, +) -> Option { + route_parameters.map(|params| RouteParametersConfig { + max_total_routing_fee_msat: params + .max_total_routing_fee_msat + .or_else(|| Some((amount_msat / 100).saturating_add(50_000))), + ..params + }) +} + pub(crate) fn proto_to_node_custom_tlv(proto: &ProtoCustomTlvRecord) -> NodeCustomTlvRecord { NodeCustomTlvRecord { type_num: proto.type_num, value: proto.value.to_vec() } } @@ -198,6 +209,47 @@ mod tests { } } + #[test] + fn missing_routing_fee_limit_defaults_to_one_percent_plus_fifty_sats() { + let params = RouteParametersConfig { + max_total_routing_fee_msat: None, + max_total_cltv_expiry_delta: 144, + max_path_count: 3, + max_channel_saturation_power_of_half: 1, + }; + + let resolved = with_default_max_routing_fee(Some(params), 1_000_000).unwrap(); + assert_eq!(resolved.max_total_routing_fee_msat, Some(10_000 + 50_000)); + assert_eq!(resolved.max_total_cltv_expiry_delta, 144); + assert_eq!(resolved.max_path_count, 3); + assert_eq!(resolved.max_channel_saturation_power_of_half, 1); + + let resolved = with_default_max_routing_fee(Some(params), 0).unwrap(); + assert_eq!(resolved.max_total_routing_fee_msat, Some(50_000)); + + let resolved = with_default_max_routing_fee(Some(params), u64::MAX).unwrap(); + assert_eq!(resolved.max_total_routing_fee_msat, Some(u64::MAX / 100 + 50_000)); + } + + #[test] + fn explicit_routing_fee_limit_is_kept() { + for limit in [0, 1_000, u64::MAX] { + let params = RouteParametersConfig { + max_total_routing_fee_msat: Some(limit), + max_total_cltv_expiry_delta: 1008, + max_path_count: 10, + max_channel_saturation_power_of_half: 2, + }; + let resolved = with_default_max_routing_fee(Some(params), 1_000_000).unwrap(); + assert_eq!(resolved.max_total_routing_fee_msat, Some(limit)); + } + } + + #[test] + fn absent_route_parameters_stay_absent() { + assert!(with_default_max_routing_fee(None, 1_000_000).is_none()); + } + #[test] fn payment_id_is_parsed() { let payment_id = "01".repeat(PaymentId::LENGTH); diff --git a/ldk-server/src/api/spontaneous_send.rs b/ldk-server/src/api/spontaneous_send.rs index 80875a3f..544bec36 100644 --- a/ldk-server/src/api/spontaneous_send.rs +++ b/ldk-server/src/api/spontaneous_send.rs @@ -18,7 +18,10 @@ use ldk_server_grpc::api::{SpontaneousSendRequest, SpontaneousSendResponse}; use crate::api::error::LdkServerError; use crate::api::error::LdkServerErrorCode::InvalidRequestError; -use crate::api::{build_route_parameters_config_from_proto, proto_to_node_custom_tlv}; +use crate::api::{ + build_route_parameters_config_from_proto, proto_to_node_custom_tlv, + with_default_max_routing_fee, +}; use crate::service::Context; pub(crate) async fn handle_spontaneous_send_request( @@ -28,7 +31,10 @@ pub(crate) async fn handle_spontaneous_send_request( LdkServerError::new(InvalidRequestError, "Invalid node_id provided.".to_string()) })?; - let route_parameters = build_route_parameters_config_from_proto(request.route_parameters)?; + let route_parameters = with_default_max_routing_fee( + build_route_parameters_config_from_proto(request.route_parameters)?, + request.amount_msat, + ); let preimage = request .preimage