From 726a340bd02506a99a0a6acd3f419274ef0534d9 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Thu, 5 Feb 2026 10:47:28 +0200 Subject: [PATCH 1/2] initial naive probing implementation Signed-off-by: dzdidi --- Cargo.lock | 3 ++- src/main.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe358dc..22f4fdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -514,6 +514,7 @@ checksum = "e6d78990de56ca75c5535c3f8e6f86b183a1aa8f521eb32afb9e8181f3bd91d7" dependencies = [ "bitcoin", "lightning", + "tokio", "windows-sys", ] diff --git a/src/main.rs b/src/main.rs index 1b1a2ac..e49802d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use bitcoin::blockdata::transaction::Transaction; use bitcoin::consensus::encode; use bitcoin::io; use bitcoin::network::Network; +use bitcoin::secp256k1::PublicKey; use bitcoin::BlockHash; use bitcoin_bech32::WitnessProgram; use disk::{INBOUND_PAYMENTS_FNAME, OUTBOUND_PAYMENTS_FNAME}; @@ -33,7 +34,10 @@ use lightning::onion_message::messenger::{ }; use lightning::routing::gossip; use lightning::routing::gossip::{NodeId, P2PGossipSync}; -use lightning::routing::router::DefaultRouter; +use lightning::routing::router::{ + DefaultRouter, PaymentParameters, RouteParameters, ScorerAccountingForInFlightHtlcs, +}; +use lightning::routing::scoring::ProbabilisticScorer; use lightning::routing::scoring::ProbabilisticScoringFeeParameters; use lightning::sign::{EntropySource, InMemorySigner, KeysManager, NodeSigner}; use lightning::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; @@ -209,6 +213,55 @@ pub(crate) type OutputSweeper = ldk_sweep::OutputSweeper< // Needed due to rust-lang/rust#63033. struct OutputSweeperWrapper(Arc); +fn send_rand_probe( + channel_manager: &ChannelManager, graph: &NetworkGraph, logger: &disk::FilesystemLogger, + scorer: &RwLock, Arc>>, +) { + let rcpt = { + let lck = graph.read_only(); + if lck.nodes().is_empty() { + return; + } + let mut it = + lck.nodes().unordered_iter().skip(::rand::random::() % lck.nodes().len()); + it.next().unwrap().0.clone() + }; + let amt = ::rand::random::() % 500_000_000; + if let Ok(pk) = bitcoin::secp256k1::PublicKey::from_slice(rcpt.as_slice()) { + send_probe(channel_manager, pk, graph, logger, amt, scorer); + } +} + +fn send_probe( + channel_manager: &ChannelManager, recipient: PublicKey, graph: &NetworkGraph, + logger: &disk::FilesystemLogger, amt_msat: u64, + scorer: &RwLock, Arc>>, +) { + let chans = channel_manager.list_usable_channels(); + let chan_refs = chans.iter().map(|a| a).collect::>(); + let mut payment_params = PaymentParameters::from_node_id(recipient, 144); + payment_params.max_path_count = 1; + let in_flight_htlcs = channel_manager.compute_inflight_htlcs(); + let scorer = scorer.read().unwrap(); + let inflight_scorer = ScorerAccountingForInFlightHtlcs::new(&scorer, &in_flight_htlcs); + let score_params: ProbabilisticScoringFeeParameters = Default::default(); + let route_res = lightning::routing::router::find_route( + &channel_manager.get_our_node_id(), + &RouteParameters::from_payment_params_and_value(payment_params, amt_msat), + &graph, + Some(&chan_refs), + logger, + &inflight_scorer, + &score_params, + &[32; 32], + ); + if let Ok(route) = route_res { + for path in route.paths { + let _ = channel_manager.send_probe(path); + } + } +} + fn handle_ldk_events<'a>( channel_manager: Arc, bitcoind_client: &'a BitcoindClient, network_graph: &'a NetworkGraph, keys_manager: &'a KeysManager, @@ -1158,6 +1211,19 @@ async fn start_ldk() { Arc::clone(&output_sweeper), )); + // Regularly probe + let probing_cm = Arc::clone(&channel_manager); + let probing_graph = Arc::clone(&network_graph); + let probing_logger = Arc::clone(&logger); + let probing_scorer = Arc::clone(&scorer); + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(1)); + loop { + interval.tick().await; + send_rand_probe(&*probing_cm, &*probing_graph, &*probing_logger, &*probing_scorer); + } + }); + // Start the CLI. let cli_channel_manager = Arc::clone(&channel_manager); let cli_chain_monitor = Arc::clone(&chain_monitor); From dc117bace1c4e33ba753cb2d77a88afa26bbb6d7 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Thu, 5 Feb 2026 11:03:20 +0200 Subject: [PATCH 2/2] solo implementation with 5.2 Codex Signed-off-by: dzdidi --- Cargo.toml | 1 + README.md | 13 +++++++ prober_config.json.example | 8 +++++ src/main.rs | 73 ++++++++++++++++++++++++++++---------- 4 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 prober_config.json.example diff --git a/Cargo.toml b/Cargo.toml index 91750b0..ddede71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ libc = "0.2" chrono = { version = "0.4", default-features = false, features = ["clock"] } rand = "0.4" serde_json = { version = "1.0" } +serde = { version = "1.0", features = ["derive"] } tokio = { version = "1", features = [ "io-util", "macros", "rt", "rt-multi-thread", "sync", "net", "time", "io-std" ] } [profile.release] diff --git a/README.md b/README.md index 2799593..d6db4ea 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,19 @@ cargo run :@:/prober_config.json +``` + +`probe_peers` pubkey of peers you are probing. + +`probe_interval_sec` how often to probe, in seconds. + +`max_amount_msats` maximum amount to probe, in millisatoshis. + ## License Licensed under either: diff --git a/prober_config.json.example b/prober_config.json.example new file mode 100644 index 0000000..40ff698 --- /dev/null +++ b/prober_config.json.example @@ -0,0 +1,8 @@ +{ + "probe_interval_sec" : 10, + "probe_peers" : [ + "03cc91150efc4bbe8d9f8ced06fbc81ad50b076b9759139e4c2d7a8380566da7e1", + "037d8e050899fa0732fdd6fc1e0d5d8d685c2093932a7e10dc5e6fab8a34ed1c43" + ], + "max_amount_msats": 500000000 +} diff --git a/src/main.rs b/src/main.rs index e49802d..207a0ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,7 @@ use lightning_dns_resolver::OMDomainResolver; use lightning_net_tokio::SocketDescriptor; use lightning_persister::fs_store::FilesystemStore; use rand::{thread_rng, Rng}; +use serde::Deserialize; use std::collections::HashMap as StdHashMap; use std::convert::TryInto; use std::fmt; @@ -77,6 +78,13 @@ pub(crate) enum HTLCStatus { Failed, } +#[derive(Deserialize)] +struct ProbeConfig { + probe_interval_sec: u64, + probe_peers: Vec, + max_amount_msats: u64, +} + impl_writeable_tlv_based_enum!(HTLCStatus, (0, Pending) => {}, (1, Succeeded) => {}, @@ -213,21 +221,20 @@ pub(crate) type OutputSweeper = ldk_sweep::OutputSweeper< // Needed due to rust-lang/rust#63033. struct OutputSweeperWrapper(Arc); -fn send_rand_probe( +fn prepare_probe( channel_manager: &ChannelManager, graph: &NetworkGraph, logger: &disk::FilesystemLogger, scorer: &RwLock, Arc>>, + pub_key_hex: &str, probe_amount: u64, ) { - let rcpt = { - let lck = graph.read_only(); - if lck.nodes().is_empty() { - return; - } - let mut it = - lck.nodes().unordered_iter().skip(::rand::random::() % lck.nodes().len()); - it.next().unwrap().0.clone() + if probe_amount == 0 { + return; + } + let amt = ::rand::random::() % probe_amount; + let pub_key_bytes = match hex_utils::to_vec(pub_key_hex) { + Some(bytes) => bytes, + None => return, }; - let amt = ::rand::random::() % 500_000_000; - if let Ok(pk) = bitcoin::secp256k1::PublicKey::from_slice(rcpt.as_slice()) { + if let Ok(pk) = bitcoin::secp256k1::PublicKey::from_slice(&pub_key_bytes) { send_probe(channel_manager, pk, graph, logger, amt, scorer); } } @@ -262,6 +269,14 @@ fn send_probe( } } +fn read_probe_file(ldk_data_dir: &str) -> Result { + let prober_file = format!("{}/prober_config.json", ldk_data_dir); + let file = fs::read_to_string(prober_file)?; + let config: ProbeConfig = serde_json::from_str(&file) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; + Ok(config) +} + fn handle_ldk_events<'a>( channel_manager: Arc, bitcoind_client: &'a BitcoindClient, network_graph: &'a NetworkGraph, keys_manager: &'a KeysManager, @@ -1216,13 +1231,35 @@ async fn start_ldk() { let probing_graph = Arc::clone(&network_graph); let probing_logger = Arc::clone(&logger); let probing_scorer = Arc::clone(&scorer); - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(1)); - loop { - interval.tick().await; - send_rand_probe(&*probing_cm, &*probing_graph, &*probing_logger, &*probing_scorer); - } - }); + match read_probe_file(&ldk_data_dir) { + Ok(probe_config) => { + if probe_config.probe_peers.is_empty() { + println!("WARNING: prober_config.json has no probe_peers. Probing disabled."); + } else { + let mut index = 0usize; + tokio::spawn(async move { + let mut interval = + tokio::time::interval(Duration::from_secs(probe_config.probe_interval_sec)); + loop { + interval.tick().await; + if index >= probe_config.probe_peers.len() { + index = 0; + } + prepare_probe( + &*probing_cm, + &*probing_graph, + &*probing_logger, + &*probing_scorer, + &probe_config.probe_peers[index], + probe_config.max_amount_msats, + ); + index += 1; + } + }); + } + }, + Err(_) => println!("WARNING: prober_config.json is missing. Probing disabled."), + }; // Start the CLI. let cli_channel_manager = Arc::clone(&channel_manager);