Skip to content

Commit b57aa56

Browse files
committed
probing: change uniffi docs to be as ordinary
Uniffi documentation is equal to the documentation for "ordinary" code. Also we add a `scorer` reference to `HighDegreeStrategy`, so it doesn't apply global fee penalties and probing node can make a payments which would take "best" routes.
1 parent 51808c0 commit b57aa56

5 files changed

Lines changed: 138 additions & 28 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: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ use crate::message_handler::NodeCustomMessageHandler;
7878
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
7979
use crate::peer_store::PeerStore;
8080
use crate::probing::{
81-
HighDegreeStrategy, Prober, ProbingConfig, ProbingStrategy, ProbingStrategyKind, RandomWalkStrategy,
81+
HighDegreeStrategy, Prober, ProbingConfig, ProbingStrategy, ProbingStrategyKind,
82+
RandomWalkStrategy,
8283
};
8384
use crate::runtime::{Runtime, RuntimeSpawner};
8485
use crate::tx_broadcaster::TransactionBroadcaster;
@@ -1821,10 +1822,7 @@ fn build_with_store_internal(
18211822
},
18221823
}
18231824

1824-
let mut scoring_fee_params = ProbabilisticScoringFeeParameters::default();
1825-
if let Some(penalty) = probing_config.and_then(|c| c.diversity_penalty_msat) {
1826-
scoring_fee_params.probing_diversity_penalty_msat = penalty;
1827-
}
1825+
let scoring_fee_params = ProbabilisticScoringFeeParameters::default();
18281826
let router = Arc::new(DefaultRouter::new(
18291827
Arc::clone(&network_graph),
18301828
Arc::clone(&logger),
@@ -2202,10 +2200,23 @@ fn build_with_store_internal(
22022200
let prober = probing_config.map(|probing_cfg| {
22032201
let strategy: Arc<dyn ProbingStrategy> = match &probing_cfg.kind {
22042202
ProbingStrategyKind::HighDegree { top_node_count } => {
2203+
// Dedicated router for probing so the diversity penalty doesn't interfere
2204+
// with real payments; shares the scorer so probe results still train it.
2205+
let mut probing_fee_params = ProbabilisticScoringFeeParameters::default();
2206+
if let Some(penalty) = probing_cfg.diversity_penalty_msat {
2207+
probing_fee_params.probing_diversity_penalty_msat = penalty;
2208+
}
2209+
let probing_router = Arc::new(DefaultRouter::new(
2210+
Arc::clone(&network_graph),
2211+
Arc::clone(&logger),
2212+
Arc::clone(&keys_manager),
2213+
Arc::clone(&scorer),
2214+
probing_fee_params,
2215+
));
22052216
Arc::new(HighDegreeStrategy::new(
22062217
Arc::clone(&network_graph),
22072218
Arc::clone(&channel_manager),
2208-
Arc::clone(&router),
2219+
probing_router,
22092220
*top_node_count,
22102221
DEFAULT_MIN_PROBE_AMOUNT_MSAT,
22112222
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: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -266,42 +266,35 @@ 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.
@@ -759,9 +752,12 @@ impl Prober {
759752
.list_recent_payments()
760753
.into_iter()
761754
.filter_map(|p| match p {
762-
RecentPaymentDetails::Pending { is_probe: true, total_msat, .. } => {
763-
Some(total_msat)
764-
},
755+
RecentPaymentDetails::Pending {
756+
is_probe: true,
757+
total_msat,
758+
pending_fee_msat,
759+
..
760+
} => Some(total_msat + pending_fee_msat.unwrap_or(0)),
765761
_ => None,
766762
})
767763
.sum();

tests/probing_tests.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
// Verifies locked_msat rises when a probe is dispatched and returns
77
// to zero once the probe resolves.
88
//
9+
// locked_msat_accounts_for_routing_fees
10+
// Asserts the exact locked_msat (delivered amount + per-hop fee) for a single
11+
// in-flight probe, proving fees are tracked and not just the delivered amount.
12+
//
913
// exhausted_probe_budget_blocks_new_probes
1014
// Samples locked_msat across multiple probe cycles and asserts it never
1115
// exceeds the configured max_locked_msat budget cap.
@@ -200,6 +204,93 @@ async fn probe_budget_increments_and_decrements() {
200204
node_c.stop().unwrap();
201205
}
202206

207+
/// Verifies that `locked_msat` accounts for routing fees, not just the delivered amount:
208+
/// a probe along A→B→C locks `delivered amount + per-hop fee` on the first-hop channel.
209+
///
210+
/// The budget is sized to exactly one probe's worth, so at most one probe is in flight and
211+
/// the observed `locked_msat` is deterministic. The existing budget test only checks that it
212+
/// is non-zero; this asserts the precise value, which a fees-excluded accounting would miss.
213+
#[tokio::test(flavor = "multi_thread")]
214+
async fn locked_msat_accounts_for_routing_fees() {
215+
// First hop carries the delivered amount plus this per-hop fee (see `build_probe_path`).
216+
const FIRST_HOP_FEE_MSAT: u64 = 1000;
217+
const LOCKED_PER_PROBE_MSAT: u64 = PROBE_AMOUNT_MSAT + FIRST_HOP_FEE_MSAT;
218+
219+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
220+
let chain_source = random_chain_source(&bitcoind, &electrsd);
221+
222+
let node_b = setup_node(&chain_source, random_config(false));
223+
let node_c = setup_node(&chain_source, random_config(false));
224+
225+
let mut config_a = random_config(false);
226+
let strategy = FixedPathStrategy::new();
227+
config_a.probing = Some(
228+
ProbingConfigBuilder::custom(strategy.clone())
229+
.interval(Duration::from_millis(PROBING_INTERVAL_MILLISECONDS))
230+
// Budget for exactly one in-flight probe so locked_msat is deterministic.
231+
.max_locked_msat(LOCKED_PER_PROBE_MSAT)
232+
.build(),
233+
);
234+
let node_a = setup_node(&chain_source, config_a);
235+
236+
let addr_a = node_a.onchain_payment().new_address().unwrap();
237+
let addr_b = node_b.onchain_payment().new_address().unwrap();
238+
premine_and_distribute_funds(
239+
&bitcoind.client,
240+
&electrsd.client,
241+
vec![addr_a, addr_b],
242+
Amount::from_sat(2_000_000),
243+
)
244+
.await;
245+
node_a.sync_wallets().unwrap();
246+
node_b.sync_wallets().unwrap();
247+
248+
open_channel(&node_a, &node_b, 1_000_000, true, &electrsd).await;
249+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await;
250+
node_b.sync_wallets().unwrap();
251+
open_channel(&node_b, &node_c, 1_000_000, true, &electrsd).await;
252+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
253+
254+
node_a.sync_wallets().unwrap();
255+
node_b.sync_wallets().unwrap();
256+
node_c.sync_wallets().unwrap();
257+
258+
expect_channel_ready_event!(node_a, node_b.node_id());
259+
expect_event!(node_b, ChannelReady);
260+
expect_event!(node_b, ChannelReady);
261+
expect_event!(node_c, ChannelReady);
262+
263+
strategy.set_path(build_probe_path(&node_a, &node_b, &node_c, PROBE_AMOUNT_MSAT));
264+
wait_for_channel_ready_to_send(&node_a, &node_b, LOCKED_PER_PROBE_MSAT).await;
265+
wait_for_channel_ready_to_send(&node_b, &node_c, PROBE_AMOUNT_MSAT).await;
266+
strategy.start_probing();
267+
268+
// Capture locked_msat the moment the first probe goes in flight. With a single-probe
269+
// budget the value is only ever 0 or exactly one probe's worth, so the first non-zero
270+
// reading is the full first-hop HTLC.
271+
let locked = tokio::time::timeout(Duration::from_secs(30), async {
272+
loop {
273+
let locked = node_a.prober().unwrap().locked_msat();
274+
if locked > 0 {
275+
break locked;
276+
}
277+
tokio::time::sleep(Duration::from_millis(100)).await;
278+
}
279+
})
280+
.await
281+
.expect("locked_msat never increased — no probe was dispatched");
282+
283+
assert_eq!(
284+
locked, LOCKED_PER_PROBE_MSAT,
285+
"locked_msat must equal the delivered amount plus routing fees, not just the delivered amount"
286+
);
287+
288+
strategy.stop_probing();
289+
node_a.stop().unwrap();
290+
node_b.stop().unwrap();
291+
node_c.stop().unwrap();
292+
}
293+
203294
/// Verifies that `locked_msat` is restored after the node is stopped and restarted
204295
/// while a probe is still in flight.
205296
///

0 commit comments

Comments
 (0)