Skip to content

Commit fd8ab95

Browse files
Add HighCapacityStrategy implementing ProbingStrategy
- Add `HighCapacityStrategy` that selects top-N nodes by aggregate capacity from `NetworkGraph`. - Add lightweight caching with configurable reuse limit to avoid frequent graph scans. - Wireable from builder via `set_probing_service_with_high_capacity_strategy`
1 parent 6420f06 commit fd8ab95

File tree

4 files changed

+348
-3
lines changed

4 files changed

+348
-3
lines changed

src/builder.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger};
6666
use crate::message_handler::NodeCustomMessageHandler;
6767
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
6868
use crate::peer_store::PeerStore;
69-
use crate::probing::{ProbingService, ProbingStrategy};
69+
use crate::probing::{HighCapacityStrategy, ProbingService, ProbingStrategy};
7070
use crate::runtime::Runtime;
7171
use crate::tx_broadcaster::TransactionBroadcaster;
7272
use crate::types::{
@@ -144,6 +144,7 @@ struct ProbingServiceConfig {
144144

145145
pub enum ProbingStrategyConfig {
146146
Custom { strategy: Arc<dyn ProbingStrategy + Send + Sync> },
147+
HighCapacity { max_targets_per_cycle: usize, target_cache_reuse_limit: usize },
147148
}
148149

149150
impl fmt::Debug for ProbingStrategyConfig {
@@ -152,6 +153,14 @@ impl fmt::Debug for ProbingStrategyConfig {
152153
Self::Custom { .. } => {
153154
f.debug_struct("Custom").field("strategy", &"<ProbingStrategy>").finish()
154155
},
156+
Self::HighCapacity {
157+
max_targets_per_cycle: max_targets,
158+
target_cache_reuse_limit: max_reloads,
159+
} => f
160+
.debug_struct("HighCapacity")
161+
.field("max_targets", max_targets)
162+
.field("max_reloads", max_reloads)
163+
.finish(),
155164
}
156165
}
157166
}
@@ -160,6 +169,13 @@ impl Clone for ProbingStrategyConfig {
160169
fn clone(&self) -> Self {
161170
match self {
162171
Self::Custom { strategy } => Self::Custom { strategy: Arc::clone(strategy) },
172+
Self::HighCapacity {
173+
max_targets_per_cycle: max_targets,
174+
target_cache_reuse_limit: max_reloads,
175+
} => Self::HighCapacity {
176+
max_targets_per_cycle: *max_targets,
177+
target_cache_reuse_limit: *max_reloads,
178+
},
163179
}
164180
}
165181
}
@@ -548,6 +564,33 @@ impl NodeBuilder {
548564
self
549565
}
550566

567+
/// Configures the probing service with the built-in high-capacity strategy.
568+
///
569+
/// Targets peers with the highest total channel capacity to assess liquidity
570+
/// on the most significant network routes.
571+
///
572+
/// # Parameters
573+
/// * `probing_interval_secs` - Seconds between probing cycles
574+
/// * `probing_amount_msat` - Amount in milli-satoshis per probe
575+
/// * `max_targets_per_cycle` - Maximum peers to probe each cycle
576+
/// * `target_cache_reuse_limit` - Number of cycles to reuse targets before refreshing.
577+
/// Acts as a cache: targets are reloaded from the network graph only after this many cycles,
578+
/// reducing overhead while adapting to network changes.
579+
pub fn set_probing_service_with_high_capacity_strategy(
580+
&mut self, probing_interval_secs: u64, probing_amount_msat: u64,
581+
max_targets_per_cycle: usize, target_cache_reuse_limit: usize,
582+
) -> &mut Self {
583+
self.probing_service_config = Some(ProbingServiceConfig {
584+
probing_interval_secs,
585+
probing_amount_msat,
586+
strategy: ProbingStrategyConfig::HighCapacity {
587+
max_targets_per_cycle,
588+
target_cache_reuse_limit,
589+
},
590+
});
591+
self
592+
}
593+
551594
/// Sets the used storage directory path.
552595
pub fn set_storage_dir_path(&mut self, storage_dir_path: String) -> &mut Self {
553596
self.config.storage_dir_path = storage_dir_path;
@@ -1060,6 +1103,30 @@ impl ArcedNodeBuilder {
10601103
);
10611104
}
10621105

1106+
/// Configures the probing service with the built-in high-capacity strategy.
1107+
///
1108+
/// Targets peers with the highest total channel capacity to assess liquidity
1109+
/// on the most significant network routes.
1110+
///
1111+
/// # Parameters
1112+
/// * `probing_interval_secs` - Seconds between probing cycles
1113+
/// * `probing_amount_msat` - Amount in milli-satoshis per probe
1114+
/// * `max_targets_per_cycle` - Maximum peers to probe each cycle
1115+
/// * `target_cache_reuse_limit` - Number of cycles to reuse targets before refreshing.
1116+
/// Acts as a cache: targets are reloaded from the network graph only after this many cycles,
1117+
/// reducing overhead while adapting to network changes.
1118+
pub fn set_probing_service_with_high_capacity_strategy(
1119+
&self, probing_interval_secs: u64, probing_amount_msat: u64, max_targets_per_cycle: usize,
1120+
target_cache_reuse_limit: usize,
1121+
) {
1122+
self.inner.write().unwrap().set_probing_service_with_high_capacity_strategy(
1123+
probing_interval_secs,
1124+
probing_amount_msat,
1125+
max_targets_per_cycle,
1126+
target_cache_reuse_limit,
1127+
);
1128+
}
1129+
10631130
/// Sets the used storage directory path.
10641131
pub fn set_storage_dir_path(&self, storage_dir_path: String) {
10651132
self.inner.write().unwrap().set_storage_dir_path(storage_dir_path);
@@ -1854,6 +1921,14 @@ fn build_with_store_internal(
18541921
let probing_service = if let Some(pro_ser) = probing_service_config {
18551922
let strategy: Arc<dyn ProbingStrategy + Send + Sync> = match &pro_ser.strategy {
18561923
ProbingStrategyConfig::Custom { strategy } => Arc::clone(strategy),
1924+
ProbingStrategyConfig::HighCapacity {
1925+
max_targets_per_cycle: max_targets,
1926+
target_cache_reuse_limit: max_reloads,
1927+
} => Arc::new(HighCapacityStrategy::new(
1928+
Arc::clone(&network_graph),
1929+
*max_targets,
1930+
*max_reloads,
1931+
)),
18571932
};
18581933
Some(Arc::new(ProbingService::new(
18591934
pro_ser.probing_interval_secs,

src/graph.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ use lightning::routing::gossip::RoutingFees;
1717
#[cfg(not(feature = "uniffi"))]
1818
use lightning::routing::gossip::{ChannelInfo, NodeInfo};
1919

20+
use std::cmp::Reverse;
21+
use std::collections::{BinaryHeap, HashMap};
22+
2023
use crate::types::Graph;
2124

2225
/// Represents the network as nodes and channels between them.
@@ -48,6 +51,38 @@ impl NetworkGraph {
4851
pub fn node(&self, node_id: &NodeId) -> Option<NodeInfo> {
4952
self.inner.read_only().nodes().get(node_id).cloned().map(|n| n.into())
5053
}
54+
55+
/// Selects nodes with the highest total channel capacity in the network.
56+
pub fn select_highest_capacity_nodes(&self, quantity_nodes: usize) -> Vec<NodeId> {
57+
// Calculate total capacity for each node by summing all their channel capacities
58+
let node_capacities = self.inner.read_only().channels().unordered_iter().fold(
59+
HashMap::new(),
60+
|mut acc, (_, chan_info)| {
61+
let cap = chan_info.capacity_sats.unwrap_or(0);
62+
*acc.entry(chan_info.node_one).or_insert(0) += cap;
63+
*acc.entry(chan_info.node_two).or_insert(0) += cap;
64+
acc
65+
},
66+
);
67+
68+
// Use a min-heap to efficiently track the top N nodes by capacity
69+
node_capacities
70+
.into_iter()
71+
.fold(BinaryHeap::with_capacity(quantity_nodes), |mut top_heap, (node_id, cap)| {
72+
if top_heap.len() < quantity_nodes {
73+
top_heap.push(Reverse((cap, node_id)));
74+
} else if let Some(Reverse((min_cap, _))) = top_heap.peek() {
75+
if cap > *min_cap {
76+
top_heap.pop();
77+
top_heap.push(Reverse((cap, node_id)));
78+
}
79+
}
80+
top_heap
81+
})
82+
.into_iter()
83+
.map(|Reverse((_, node_id))| node_id)
84+
.collect()
85+
}
5186
}
5287

5388
/// Details about a channel (both directions).

src/probing.rs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use bitcoin::secp256k1::PublicKey;
2-
use std::sync::{Arc, RwLock};
2+
use std::sync::{Arc, Mutex, RwLock};
33

44
use crate::{
55
config::Config,
6+
graph::NetworkGraph,
67
logger::{log_debug, log_error, LdkLogger, Logger},
78
payment::SpontaneousPayment,
8-
types::{ChannelManager, KeysManager, PaymentStore},
9+
types::{ChannelManager, Graph, KeysManager, PaymentStore},
910
};
1011

1112
/// Trait for probing strategies to select targets for liquidity assessment.
@@ -19,6 +20,70 @@ pub trait ProbingStrategy {
1920
fn next_target(&self) -> Option<PublicKey>;
2021
}
2122

23+
/// Simple strategy that selects targets based on highest channel capacity.
24+
pub struct HighCapacityStrategy {
25+
network_graph: Arc<Graph>,
26+
max_targets_per_cycle: usize,
27+
target_cache_reuse_limit: usize,
28+
targets: Mutex<Vec<PublicKey>>,
29+
target_index: Mutex<usize>,
30+
uses_since_load: Mutex<usize>,
31+
}
32+
33+
impl HighCapacityStrategy {
34+
pub fn new(network_graph: Arc<Graph>, max_targets: usize, max_reloads: usize) -> Self {
35+
Self {
36+
network_graph,
37+
max_targets_per_cycle: max_targets,
38+
target_cache_reuse_limit: max_reloads,
39+
targets: Mutex::new(Vec::new()),
40+
target_index: Mutex::new(0),
41+
uses_since_load: Mutex::new(0),
42+
}
43+
}
44+
45+
fn network_graph(&self) -> NetworkGraph {
46+
NetworkGraph::new(Arc::clone(&self.network_graph))
47+
}
48+
}
49+
50+
impl ProbingStrategy for HighCapacityStrategy {
51+
fn load_targets(&self) {
52+
let mut targets = self.targets.lock().unwrap();
53+
let mut uses_since_load = self.uses_since_load.lock().unwrap();
54+
55+
if !targets.is_empty() && *uses_since_load < self.target_cache_reuse_limit {
56+
*uses_since_load += 1;
57+
return;
58+
}
59+
60+
let network = self.network_graph();
61+
let highest_capacity_nodes =
62+
network.select_highest_capacity_nodes(self.max_targets_per_cycle);
63+
*targets =
64+
highest_capacity_nodes.iter().filter_map(|node_id| node_id.as_pubkey().ok()).collect();
65+
66+
let mut target_index = self.target_index.lock().unwrap();
67+
*target_index = 0;
68+
*uses_since_load = 0;
69+
}
70+
71+
fn next_target(&self) -> Option<PublicKey> {
72+
let mut target_index = self.target_index.lock().unwrap();
73+
let targets = self.targets.lock().unwrap();
74+
75+
if *target_index < targets.len() {
76+
let pk = targets[*target_index];
77+
*target_index += 1;
78+
Some(pk)
79+
} else {
80+
// reset index for next cycle
81+
*target_index = 0;
82+
None
83+
}
84+
}
85+
}
86+
2287
/// Configuration for the probing service used to evaluate channel liquidity by sending pre-flight
2388
/// probes to peers and routes.
2489
pub struct ProbingService {

0 commit comments

Comments
 (0)