Skip to content

Commit 324dec0

Browse files
randomloginclaude
andcommitted
Add probing service tests
Add integration tests that verify the probing service fires probes on the configured interval and respects the locked-msat budget cap. Shared helpers in tests/common are extended with probing-aware setup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7dfe041 commit 324dec0

6 files changed

Lines changed: 610 additions & 53 deletions

File tree

bindings/ldk_node.udl

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ typedef interface NodeEntropy;
1515

1616
typedef interface ProbingConfig;
1717

18-
typedef interface ProbingConfigBuilder;
19-
2018
typedef enum WordCount;
2119

2220
[Remote]
@@ -36,6 +34,18 @@ interface LogWriter {
3634
void log(LogRecord record);
3735
};
3836

37+
interface ProbingConfigBuilder {
38+
[Name=high_degree]
39+
constructor(u64 top_node_count);
40+
[Name=random_walk]
41+
constructor(u64 max_hops);
42+
void set_interval(u64 secs);
43+
void set_max_locked_msat(u64 max_msat);
44+
void set_diversity_penalty_msat(u64 penalty_msat);
45+
void set_cooldown(u64 secs);
46+
ProbingConfig build();
47+
};
48+
3949
interface Builder {
4050
constructor();
4151
[Name=from_config]

src/builder.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ use crate::message_handler::NodeCustomMessageHandler;
7676
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
7777
use crate::peer_store::PeerStore;
7878
use crate::probing::{
79-
HighDegreeStrategy, Prober, ProbingConfig, ProbingStrategy, ProbingStrategyKind, RandomWalkStrategy,
79+
HighDegreeStrategy, Prober, ProbingConfig, ProbingStrategy, ProbingStrategyKind,
80+
RandomWalkStrategy,
8081
};
8182
use crate::runtime::{Runtime, RuntimeSpawner};
8283
use crate::tx_broadcaster::TransactionBroadcaster;
@@ -1188,7 +1189,7 @@ impl ArcedNodeBuilder {
11881189
///
11891190
/// [`ProbingConfigBuilder`]: crate::probing::ProbingConfigBuilder
11901191
pub fn set_probing_config(&self, config: Arc<ProbingConfig>) {
1191-
self.inner.write().unwrap().set_probing_config((*config).clone());
1192+
self.inner.write().expect("lock").set_probing_config((*config).clone());
11921193
}
11931194

11941195
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
@@ -1805,10 +1806,7 @@ fn build_with_store_internal(
18051806
},
18061807
}
18071808

1808-
let mut scoring_fee_params = ProbabilisticScoringFeeParameters::default();
1809-
if let Some(penalty) = probing_config.and_then(|c| c.diversity_penalty_msat) {
1810-
scoring_fee_params.probing_diversity_penalty_msat = penalty;
1811-
}
1809+
let scoring_fee_params = ProbabilisticScoringFeeParameters::default();
18121810
let router = Arc::new(DefaultRouter::new(
18131811
Arc::clone(&network_graph),
18141812
Arc::clone(&logger),
@@ -2172,10 +2170,23 @@ fn build_with_store_internal(
21722170
let prober = probing_config.map(|probing_cfg| {
21732171
let strategy: Arc<dyn ProbingStrategy> = match &probing_cfg.kind {
21742172
ProbingStrategyKind::HighDegree { top_node_count } => {
2173+
// Dedicated router for probing so the diversity penalty doesn't interfere
2174+
// with real payments; shares the scorer so probe results still train it.
2175+
let mut probing_fee_params = ProbabilisticScoringFeeParameters::default();
2176+
if let Some(penalty) = probing_cfg.diversity_penalty_msat {
2177+
probing_fee_params.probing_diversity_penalty_msat = penalty;
2178+
}
2179+
let probing_router = Arc::new(DefaultRouter::new(
2180+
Arc::clone(&network_graph),
2181+
Arc::clone(&logger),
2182+
Arc::clone(&keys_manager),
2183+
Arc::clone(&scorer),
2184+
probing_fee_params,
2185+
));
21752186
Arc::new(HighDegreeStrategy::new(
21762187
Arc::clone(&network_graph),
21772188
Arc::clone(&channel_manager),
2178-
Arc::clone(&router),
2189+
probing_router,
21792190
*top_node_count,
21802191
DEFAULT_MIN_PROBE_AMOUNT_MSAT,
21812192
DEFAULT_MAX_PROBE_AMOUNT_MSAT,

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ use payment::{
174174
UnifiedPayment,
175175
};
176176
use peer_store::{PeerInfo, PeerStore};
177+
#[cfg(feature = "uniffi")]
178+
pub use probing::ArcedProbingConfigBuilder as ProbingConfigBuilder;
177179
use probing::{run_prober, Prober};
178180
use runtime::Runtime;
179181
pub use tokio;

src/probing.rs

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -266,56 +266,49 @@ impl ProbingConfigBuilder {
266266
}
267267
}
268268

269-
/// A UniFFI-compatible wrapper around [`ProbingConfigBuilder`] that uses interior mutability
270-
/// so it can be shared behind an `Arc` as required by the FFI object model.
269+
/// Builder for [`ProbingConfig`].
271270
///
272-
/// Instances are produced by the constructors [`new_high_degree`] and [`new_random_walk`].
273-
/// The `set_*` methods override the defaults, and [`build`] yields the resulting
274-
/// [`ProbingConfig`].
271+
/// A new instance starts from one of two strategy constructors — [`high_degree`] or
272+
/// [`random_walk`] — and is finalized through [`build`]. Optional setters in between
273+
/// override the timing and liquidity defaults.
275274
///
276-
/// [`new_high_degree`]: Self::new_high_degree
277-
/// [`new_random_walk`]: Self::new_random_walk
275+
/// [`high_degree`]: Self::high_degree
276+
/// [`random_walk`]: Self::random_walk
278277
/// [`build`]: Self::build
279278
#[cfg(feature = "uniffi")]
280-
#[derive(uniffi::Object)]
281279
pub struct ArcedProbingConfigBuilder {
282280
inner: RwLock<ProbingConfigBuilder>,
283281
}
284282

285283
#[cfg(feature = "uniffi")]
286-
#[uniffi::export]
287284
impl ArcedProbingConfigBuilder {
288285
/// Start building a config that probes toward the highest-degree nodes in the graph.
289286
///
290287
/// `top_node_count` controls how many of the most-connected nodes are cycled through.
291-
#[uniffi::constructor]
292-
pub fn new_high_degree(top_node_count: u64) -> Arc<Self> {
293-
Arc::new(Self {
294-
inner: RwLock::new(ProbingConfigBuilder::high_degree(top_node_count as usize)),
295-
})
288+
pub fn high_degree(top_node_count: u64) -> Self {
289+
Self { inner: RwLock::new(ProbingConfigBuilder::high_degree(top_node_count as usize)) }
296290
}
297291

298292
/// Start building a config that probes via random graph walks.
299293
///
300294
/// `max_hops` is the upper bound on the number of hops in a randomly constructed path.
301295
/// Values below `2` are clamped to `2`.
302-
#[uniffi::constructor]
303-
pub fn new_random_walk(max_hops: u64) -> Arc<Self> {
304-
Arc::new(Self { inner: RwLock::new(ProbingConfigBuilder::random_walk(max_hops as usize)) })
296+
pub fn random_walk(max_hops: u64) -> Self {
297+
Self { inner: RwLock::new(ProbingConfigBuilder::random_walk(max_hops as usize)) }
305298
}
306299

307300
/// Overrides the interval between probe attempts.
308301
///
309302
/// Defaults to 10 seconds.
310303
pub fn set_interval(&self, secs: u64) {
311-
self.inner.write().unwrap().interval(Duration::from_secs(secs));
304+
self.inner.write().expect("lock").interval(Duration::from_secs(secs));
312305
}
313306

314307
/// Overrides the maximum millisatoshis that may be locked in in-flight probes at any time.
315308
///
316309
/// Defaults to 100 000 000 msat (100k sats).
317310
pub fn set_max_locked_msat(&self, max_msat: u64) {
318-
self.inner.write().unwrap().max_locked_msat(max_msat);
311+
self.inner.write().expect("lock").max_locked_msat(max_msat);
319312
}
320313

321314
/// Sets the probing diversity penalty applied by the probabilistic scorer.
@@ -330,19 +323,19 @@ impl ArcedProbingConfigBuilder {
330323
///
331324
/// If unset, LDK's default of `0` (no penalty) is used.
332325
pub fn set_diversity_penalty_msat(&self, penalty_msat: u64) {
333-
self.inner.write().unwrap().diversity_penalty_msat(penalty_msat);
326+
self.inner.write().expect("lock").diversity_penalty_msat(penalty_msat);
334327
}
335328

336329
/// Sets how long a probed node stays ineligible before being probed again.
337330
///
338331
/// Only applies to [`HighDegreeStrategy`]. Defaults to 1 hour.
339332
pub fn set_cooldown(&self, secs: u64) {
340-
self.inner.write().unwrap().cooldown(Duration::from_secs(secs));
333+
self.inner.write().expect("lock").cooldown(Duration::from_secs(secs));
341334
}
342335

343336
/// Builds the [`ProbingConfig`].
344337
pub fn build(&self) -> Arc<ProbingConfig> {
345-
Arc::new(self.inner.read().unwrap().build())
338+
Arc::new(self.inner.read().expect("lock").build())
346339
}
347340
}
348341

@@ -382,7 +375,7 @@ pub struct HighDegreeStrategy {
382375
/// `path_value * liquidity_limit_multiplier`.
383376
pub liquidity_limit_multiplier: u64,
384377
/// Nodes probed recently, with the time they were last probed.
385-
recently_probed: Mutex<HashMap<PublicKey, Instant>>,
378+
recently_probed: Mutex<HashMap<NodeId, Instant>>,
386379
}
387380

388381
impl HighDegreeStrategy {
@@ -414,13 +407,8 @@ impl ProbingStrategy for HighDegreeStrategy {
414407
fn next_probe(&self) -> Option<Path> {
415408
let graph = self.network_graph.read_only();
416409

417-
let mut nodes_by_degree: Vec<(PublicKey, usize)> = graph
418-
.nodes()
419-
.unordered_iter()
420-
.filter_map(|(id, info)| {
421-
PublicKey::try_from(*id).ok().map(|pubkey| (pubkey, info.channels.len()))
422-
})
423-
.collect();
410+
let mut nodes_by_degree: Vec<(NodeId, usize)> =
411+
graph.nodes().unordered_iter().map(|(id, info)| (*id, info.channels.len())).collect();
424412

425413
if nodes_by_degree.is_empty() {
426414
return None;
@@ -438,21 +426,23 @@ impl ProbingStrategy for HighDegreeStrategy {
438426
probed.retain(|_, probed_at| now.duration_since(*probed_at) < self.cooldown);
439427

440428
// If all top nodes are on cooldown, reset and start a new cycle.
441-
let final_node = match nodes_by_degree[..top_node_count]
429+
let final_node_id = match nodes_by_degree[..top_node_count]
442430
.iter()
443-
.find(|(pubkey, _)| !probed.contains_key(pubkey))
431+
.find(|(node_id, _)| !probed.contains_key(node_id))
444432
{
445-
Some((pubkey, _)) => *pubkey,
433+
Some((node_id, _)) => *node_id,
446434
None => {
447435
probed.clear();
448436
nodes_by_degree[0].0
449437
},
450438
};
451439

452-
probed.insert(final_node, now);
440+
probed.insert(final_node_id, now);
453441
drop(probed);
454442
drop(graph);
455443

444+
let final_node = PublicKey::try_from(final_node_id).ok()?;
445+
456446
let amount_msat = random_range(self.min_amount_msat, self.max_amount_msat);
457447
let payment_params =
458448
PaymentParameters::from_node_id(final_node, DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA as u32);
@@ -471,6 +461,10 @@ impl ProbingStrategy for HighDegreeStrategy {
471461

472462
let path = route.paths.into_iter().next()?;
473463

464+
if path.hops.len() < 2 && path.blinded_tail.is_none() {
465+
return None;
466+
}
467+
474468
// Liquidity-limit check (mirrors send_preflight_probes): skip the path when the
475469
// first-hop outbound liquidity is less than path_value * liquidity_limit_multiplier.
476470
if let Some(first_hop_hop) = path.hops.first() {
@@ -711,6 +705,10 @@ impl RandomWalkStrategy {
711705

712706
hops.reverse();
713707

708+
if hops.len() < 2 {
709+
return None;
710+
}
711+
714712
// The first-hop HTLC carries amount_msat + all intermediate fees.
715713
// Verify the total fits within our live outbound limit before returning.
716714
let total_outgoing: u64 = hops.iter().map(|h| h.fee_msat).sum();
@@ -759,9 +757,12 @@ impl Prober {
759757
.list_recent_payments()
760758
.into_iter()
761759
.filter_map(|p| match p {
762-
RecentPaymentDetails::Pending { is_probe: true, total_msat, .. } => {
763-
Some(total_msat)
764-
},
760+
RecentPaymentDetails::Pending {
761+
is_probe: true,
762+
total_msat,
763+
pending_fee_msat,
764+
..
765+
} => Some(total_msat + pending_fee_msat.unwrap_or(0)),
765766
_ => None,
766767
})
767768
.sum();

0 commit comments

Comments
 (0)