From ab7075f0de79a259b628cfba5162c52e9387fe7b Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Fri, 24 Apr 2026 17:16:04 +0200 Subject: [PATCH 1/2] Add configurable scoring fee/decay parameters --- bindings/ldk_node.udl | 6 +++ src/builder.rs | 35 ++++++++++-- src/config.rs | 121 ++++++++++++++++++++++++++++++++++++++++++ src/ffi/types.rs | 5 +- 4 files changed, 163 insertions(+), 4 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c32604708..05d5ae04c 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -11,6 +11,10 @@ typedef dictionary ElectrumSyncConfig; typedef dictionary TorConfig; +typedef dictionary ScoringFeeParameters; + +typedef dictionary ScoringDecayParameters; + typedef interface NodeEntropy; typedef enum WordCount; @@ -43,6 +47,8 @@ interface Builder { void set_gossip_source_p2p(); void set_gossip_source_rgs(string rgs_server_url); void set_pathfinding_scores_source(string url); + void set_scoring_fee_params(ScoringFeeParameters scoring_fee_params); + void set_scoring_decay_params(ScoringDecayParameters scoring_decay_params); void set_liquidity_source_lsps1(PublicKey node_id, SocketAddress address, string? token); void set_liquidity_source_lsps2(PublicKey node_id, SocketAddress address, string? token); void set_storage_dir_path(string storage_dir_path); diff --git a/src/builder.rs b/src/builder.rs index b0ff1d03b..efd0e38a8 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -50,7 +50,8 @@ use crate::chain::ChainSource; use crate::config::{ default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig, - TorConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, + ScoringDecayParameters, ScoringFeeParameters, TorConfig, DEFAULT_ESPLORA_SERVER_URL, + DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, }; use crate::connection::ConnectionManager; use crate::entropy::NodeEntropy; @@ -444,6 +445,22 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to use custom probabilistic scorer fee parameters. + pub fn set_scoring_fee_params( + &mut self, scoring_fee_params: ScoringFeeParameters, + ) -> &mut Self { + self.config.scoring_fee_params = Some(scoring_fee_params); + self + } + + /// Configures the [`Node`] instance to use custom probabilistic scorer decay parameters. + pub fn set_scoring_decay_params( + &mut self, scoring_decay_params: ScoringDecayParameters, + ) -> &mut Self { + self.config.scoring_decay_params = Some(scoring_decay_params); + self + } + /// Configures the [`Node`] instance to source inbound liquidity from the given /// [bLIP-51 / LSPS1] service. /// @@ -965,6 +982,16 @@ impl ArcedNodeBuilder { self.inner.write().expect("lock").set_pathfinding_scores_source(url); } + /// Configures the [`Node`] instance to use custom probabilistic scorer fee parameters. + pub fn set_scoring_fee_params(&self, scoring_fee_params: ScoringFeeParameters) { + self.inner.write().expect("lock").set_scoring_fee_params(scoring_fee_params); + } + + /// Configures the [`Node`] instance to use custom probabilistic scorer decay parameters. + pub fn set_scoring_decay_params(&self, scoring_decay_params: ScoringDecayParameters) { + self.inner.write().expect("lock").set_scoring_decay_params(scoring_decay_params); + } + /// Configures the [`Node`] instance to source inbound liquidity from the given /// [bLIP-51 / LSPS1] service. /// @@ -1614,7 +1641,8 @@ fn build_with_store_internal( Ok(scorer) => scorer, Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { - let params = ProbabilisticScoringDecayParameters::default(); + let params: ProbabilisticScoringDecayParameters = + config.scoring_decay_params.unwrap_or_default().into(); ProbabilisticScorer::new(params, Arc::clone(&network_graph), Arc::clone(&logger)) } else { log_error!(logger, "Failed to read scoring data from store: {}", e); @@ -1639,7 +1667,8 @@ fn build_with_store_internal( }, } - let scoring_fee_params = ProbabilisticScoringFeeParameters::default(); + let scoring_fee_params: ProbabilisticScoringFeeParameters = + config.scoring_fee_params.unwrap_or_default().into(); let router = Arc::new(DefaultRouter::new( Arc::clone(&network_graph), Arc::clone(&logger), diff --git a/src/config.rs b/src/config.rs index 6f786d764..d8bda5f33 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,6 +16,9 @@ use bitcoin::Network; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; use lightning::routing::router::RouteParametersConfig; +use lightning::routing::scoring::{ + ProbabilisticScoringDecayParameters, ProbabilisticScoringFeeParameters, +}; use lightning::util::config::{ ChannelConfig as LdkChannelConfig, MaxDustHTLCExposure as LdkMaxDustHTLCExposure, UserConfig, }; @@ -194,6 +197,14 @@ pub struct Config { /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. pub route_parameters: Option, + /// Configuration options for the probabilistic routing scorer fee parameters. + /// + /// **Note:** If unset, LDK's default values will be used. + pub scoring_fee_params: Option, + /// Configuration options for the probabilistic routing scorer decay parameters. + /// + /// **Note:** If unset, LDK's default values will be used. + pub scoring_decay_params: Option, /// Configuration options for enabling peer connections via the Tor network. /// /// Setting [`TorConfig`] enables connecting to peers with OnionV3 addresses. No other connections @@ -219,12 +230,122 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), tor_config: None, route_parameters: None, + scoring_fee_params: None, + scoring_decay_params: None, node_alias: None, hrn_config: HumanReadableNamesConfig::default(), } } } +/// Configuration options for probabilistic routing scorer fee-related penalties. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct ScoringFeeParameters { + /// A fixed penalty in msats to apply to each channel. + pub base_penalty_msat: u64, + /// Multiplier used with the amount for a fixed per-channel amount penalty. + pub base_penalty_amount_multiplier_msat: u64, + /// Multiplier used with channel success probability to determine liquidity penalty. + pub liquidity_penalty_multiplier_msat: u64, + /// Multiplier used with amount and success probability for liquidity amount penalty. + pub liquidity_penalty_amount_multiplier_msat: u64, + /// Multiplier for historical liquidity-based penalties. + pub historical_liquidity_penalty_multiplier_msat: u64, + /// Amount-aware multiplier for historical liquidity-based penalties. + pub historical_liquidity_penalty_amount_multiplier_msat: u64, + /// Penalty applied for channels with large `htlc_maximum_msat`. + pub anti_probing_penalty_msat: u64, + /// Penalty applied when amount exceeds estimated available channel liquidity. + pub considered_impossible_penalty_msat: u64, + /// If true, use a linear channel success probability distribution. + pub linear_success_probability: bool, + /// Additional penalty for recently probed channels, useful for probing diversity. + pub probing_diversity_penalty_msat: u64, +} + +impl Default for ScoringFeeParameters { + fn default() -> Self { + ProbabilisticScoringFeeParameters::default().into() + } +} + +impl From for ScoringFeeParameters { + fn from(value: ProbabilisticScoringFeeParameters) -> Self { + Self { + base_penalty_msat: value.base_penalty_msat, + base_penalty_amount_multiplier_msat: value.base_penalty_amount_multiplier_msat, + liquidity_penalty_multiplier_msat: value.liquidity_penalty_multiplier_msat, + liquidity_penalty_amount_multiplier_msat: value + .liquidity_penalty_amount_multiplier_msat, + historical_liquidity_penalty_multiplier_msat: value + .historical_liquidity_penalty_multiplier_msat, + historical_liquidity_penalty_amount_multiplier_msat: value + .historical_liquidity_penalty_amount_multiplier_msat, + anti_probing_penalty_msat: value.anti_probing_penalty_msat, + considered_impossible_penalty_msat: value.considered_impossible_penalty_msat, + linear_success_probability: value.linear_success_probability, + probing_diversity_penalty_msat: value.probing_diversity_penalty_msat, + } + } +} + +impl From for ProbabilisticScoringFeeParameters { + fn from(value: ScoringFeeParameters) -> Self { + let mut ldk_value = ProbabilisticScoringFeeParameters::default(); + ldk_value.base_penalty_msat = value.base_penalty_msat; + ldk_value.base_penalty_amount_multiplier_msat = value.base_penalty_amount_multiplier_msat; + ldk_value.liquidity_penalty_multiplier_msat = value.liquidity_penalty_multiplier_msat; + ldk_value.liquidity_penalty_amount_multiplier_msat = + value.liquidity_penalty_amount_multiplier_msat; + ldk_value.historical_liquidity_penalty_multiplier_msat = + value.historical_liquidity_penalty_multiplier_msat; + ldk_value.historical_liquidity_penalty_amount_multiplier_msat = + value.historical_liquidity_penalty_amount_multiplier_msat; + ldk_value.anti_probing_penalty_msat = value.anti_probing_penalty_msat; + ldk_value.considered_impossible_penalty_msat = value.considered_impossible_penalty_msat; + ldk_value.linear_success_probability = value.linear_success_probability; + ldk_value.probing_diversity_penalty_msat = value.probing_diversity_penalty_msat; + ldk_value + } +} + +/// Configuration options for probabilistic routing scorer decay behavior. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct ScoringDecayParameters { + /// Time after which historical samples are decayed by half, in seconds. + pub historical_no_updates_half_life_secs: u64, + /// Time after which liquidity-offset bounds are decayed by half, in seconds. + pub liquidity_offset_half_life_secs: u64, +} + +impl Default for ScoringDecayParameters { + fn default() -> Self { + ProbabilisticScoringDecayParameters::default().into() + } +} + +impl From for ScoringDecayParameters { + fn from(value: ProbabilisticScoringDecayParameters) -> Self { + Self { + historical_no_updates_half_life_secs: value.historical_no_updates_half_life.as_secs(), + liquidity_offset_half_life_secs: value.liquidity_offset_half_life.as_secs(), + } + } +} + +impl From for ProbabilisticScoringDecayParameters { + fn from(value: ScoringDecayParameters) -> Self { + Self { + historical_no_updates_half_life: Duration::from_secs( + value.historical_no_updates_half_life_secs, + ), + liquidity_offset_half_life: Duration::from_secs(value.liquidity_offset_half_life_secs), + } + } +} + /// Configuration options for how our node resolves Human-Readable Names (BIP 353). /// /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki diff --git a/src/ffi/types.rs b/src/ffi/types.rs index ad293bc3e..771c0ea66 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -143,7 +143,10 @@ impl VssClientHeaderProvider for VssHeaderProviderAdapter { } use crate::builder::sanitize_alias; -pub use crate::config::{default_config, ElectrumSyncConfig, EsploraSyncConfig, TorConfig}; +pub use crate::config::{ + default_config, ElectrumSyncConfig, EsploraSyncConfig, ScoringDecayParameters, + ScoringFeeParameters, TorConfig, +}; pub use crate::entropy::{generate_entropy_mnemonic, NodeEntropy, WordCount}; use crate::error::Error; pub use crate::liquidity::LSPS1OrderStatus; From cdcb5a6a35d012bd8dbb4235cb5df81a68db0124 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Fri, 24 Apr 2026 17:20:23 +0200 Subject: [PATCH 2/2] Add unit test --- src/builder.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/builder.rs b/src/builder.rs index efd0e38a8..9d905e248 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2177,7 +2177,8 @@ pub(crate) fn sanitize_alias(alias_str: &str) -> Result { #[cfg(test)] mod tests { - use super::{sanitize_alias, BuildError, NodeAlias}; + use super::{sanitize_alias, BuildError, NodeAlias, NodeBuilder}; + use crate::config::{ScoringDecayParameters, ScoringFeeParameters}; #[test] fn sanitize_empty_node_alias() { @@ -2214,4 +2215,32 @@ mod tests { let node = sanitize_alias(alias); assert_eq!(node.err().unwrap(), BuildError::InvalidNodeAlias); } + + #[test] + fn set_scoring_params_on_builder_config() { + let mut builder = NodeBuilder::new(); + + let scoring_fee_params = ScoringFeeParameters { + base_penalty_msat: 2_048, + base_penalty_amount_multiplier_msat: 262_144, + liquidity_penalty_multiplier_msat: 10, + liquidity_penalty_amount_multiplier_msat: 20, + historical_liquidity_penalty_multiplier_msat: 30, + historical_liquidity_penalty_amount_multiplier_msat: 40, + anti_probing_penalty_msat: 50, + considered_impossible_penalty_msat: 60, + linear_success_probability: true, + probing_diversity_penalty_msat: 70, + }; + let scoring_decay_params = ScoringDecayParameters { + historical_no_updates_half_life_secs: 1234, + liquidity_offset_half_life_secs: 5678, + }; + + builder.set_scoring_fee_params(scoring_fee_params); + builder.set_scoring_decay_params(scoring_decay_params); + + assert_eq!(builder.config.scoring_fee_params, Some(scoring_fee_params)); + assert_eq!(builder.config.scoring_decay_params, Some(scoring_decay_params)); + } }