Skip to content

Commit 59dbbe6

Browse files
committed
Add pathfinder trait
1 parent fefbfc7 commit 59dbbe6

File tree

3 files changed

+143
-77
lines changed

3 files changed

+143
-77
lines changed

sim-cli/src/main.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use simln_lib::{
1010
use simple_logger::SimpleLogger;
1111
use tokio_util::task::TaskTracker;
1212

13+
// Import the pathfinder types
14+
use simln_lib::sim_node::DefaultPathFinder;
15+
1316
#[tokio::main]
1417
async fn main() -> anyhow::Result<()> {
1518
// Enable tracing if building in developer mode.
@@ -31,6 +34,9 @@ async fn main() -> anyhow::Result<()> {
3134
cli.validate(&sim_params)?;
3235

3336
let tasks = TaskTracker::new();
37+
38+
// Create the pathfinder instance
39+
let pathfinder = DefaultPathFinder;
3440

3541
let (sim, validated_activities) = if sim_params.sim_network.is_empty() {
3642
create_simulation(&cli, &sim_params, tasks.clone()).await?
@@ -46,6 +52,7 @@ async fn main() -> anyhow::Result<()> {
4652
&sim_params,
4753
tasks.clone(),
4854
interceptors,
55+
pathfinder,
4956
CustomRecords::default(),
5057
)
5158
.await?
@@ -60,4 +67,4 @@ async fn main() -> anyhow::Result<()> {
6067
sim.run(&validated_activities).await?;
6168

6269
Ok(())
63-
}
70+
}

sim-cli/src/parsing.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ use log::LevelFilter;
55
use serde::{Deserialize, Serialize};
66
use simln_lib::clock::SimulationClock;
77
use simln_lib::sim_node::{
8-
ln_node_from_graph, populate_network_graph, ChannelPolicy, CustomRecords, Interceptor,
8+
ln_node_from_graph, populate_network_graph, ChannelPolicy, CustomRecords, Interceptor, PathFinder,
99
SimGraph, SimulatedChannel,
1010
};
11+
1112
use simln_lib::{
1213
cln, cln::ClnNode, eclair, eclair::EclairNode, lnd, lnd::LndNode, serializers,
1314
ActivityDefinition, Amount, Interval, LightningError, LightningNode, NodeId, NodeInfo,
@@ -224,12 +225,13 @@ struct NodeMapping {
224225
alias_node_map: HashMap<String, NodeInfo>,
225226
}
226227

227-
pub async fn create_simulation_with_network(
228+
pub async fn create_simulation_with_network<P: for<'a> PathFinder<'a> + Clone + 'static>(
228229
cli: &Cli,
229230
sim_params: &SimParams,
230231
tasks: TaskTracker,
231232
interceptors: Vec<Arc<dyn Interceptor>>,
232233
custom_records: CustomRecords,
234+
pathfinder: P,
233235
) -> Result<(Simulation<SimulationClock>, Vec<ActivityDefinition>), anyhow::Error> {
234236
let cfg: SimulationCfg = SimulationCfg::try_from(cli)?;
235237
let SimParams {
@@ -276,7 +278,8 @@ pub async fn create_simulation_with_network(
276278
.map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?,
277279
);
278280

279-
let nodes = ln_node_from_graph(simulation_graph.clone(), routing_graph).await;
281+
// Pass the pathfinder to ln_node_from_graph
282+
let nodes = ln_node_from_graph(simulation_graph.clone(), routing_graph, pathfinder).await;
280283
let validated_activities =
281284
get_validated_activities(&nodes, nodes_info, sim_params.activity.clone()).await?;
282285

@@ -293,6 +296,7 @@ pub async fn create_simulation_with_network(
293296
))
294297
}
295298

299+
296300
/// Parses the cli options provided and creates a simulation to be run, connecting to lightning nodes and validating
297301
/// any activity described in the simulation file.
298302
pub async fn create_simulation(

simln-lib/src/sim_node.rs

Lines changed: 128 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -484,11 +484,57 @@ pub trait SimNetwork: Send + Sync {
484484
fn list_nodes(&self) -> Result<Vec<NodeInfo>, LightningError>;
485485
}
486486

487+
/// A trait for custom pathfinding implementations.
488+
pub trait PathFinder<'a>: Send + Sync {
489+
fn find_route(
490+
&self,
491+
source: &PublicKey,
492+
dest: PublicKey,
493+
amount_msat: u64,
494+
pathfinding_graph: &NetworkGraph<&'a WrappedLog>,
495+
scorer: &ProbabilisticScorer<Arc<NetworkGraph<&'a WrappedLog>>, &'a WrappedLog>,
496+
) -> Result<Route, SimulationError>;
497+
}
498+
499+
/// Default pathfinder that uses LDK's pathfinding algorithm.
500+
#[derive(Clone)]
501+
pub struct DefaultPathFinder;
502+
503+
impl<'a> PathFinder<'a> for DefaultPathFinder {
504+
fn find_route(
505+
&self,
506+
source: &PublicKey,
507+
dest: PublicKey,
508+
amount_msat: u64,
509+
pathfinding_graph: &NetworkGraph<&'a WrappedLog>,
510+
scorer: &ProbabilisticScorer<Arc<NetworkGraph<&'a WrappedLog>>, &'a WrappedLog>,
511+
) -> Result<Route, SimulationError> {
512+
find_route(
513+
source,
514+
&RouteParameters {
515+
payment_params: PaymentParameters::from_node_id(dest, 0)
516+
.with_max_total_cltv_expiry_delta(u32::MAX)
517+
.with_max_path_count(1)
518+
.with_max_channel_saturation_power_of_half(1),
519+
final_value_msat: amount_msat,
520+
max_total_routing_fee_msat: None,
521+
},
522+
pathfinding_graph,
523+
None,
524+
&WrappedLog {},
525+
scorer,
526+
&Default::default(),
527+
&[0; 32],
528+
)
529+
.map_err(|e| SimulationError::SimulatedNetworkError(e.err))
530+
}
531+
}
532+
487533
/// A wrapper struct used to implement the LightningNode trait (can be thought of as "the" lightning node). Passes
488534
/// all functionality through to a coordinating simulation network. This implementation contains both the [`SimNetwork`]
489535
/// implementation that will allow us to dispatch payments and a read-only NetworkGraph that is used for pathfinding.
490536
/// While these two could be combined, we re-use the LDK-native struct to allow re-use of their pathfinding logic.
491-
pub struct SimNode<'a, T: SimNetwork> {
537+
pub struct SimNode<'a, T: SimNetwork, P: PathFinder<'a> = DefaultPathFinder> {
492538
info: NodeInfo,
493539
/// The underlying execution network that will be responsible for dispatching payments.
494540
network: Arc<Mutex<T>>,
@@ -499,15 +545,18 @@ pub struct SimNode<'a, T: SimNetwork> {
499545
/// Probabilistic scorer used to rank paths through the network for routing. This is reused across
500546
/// multiple payments to maintain scoring state.
501547
scorer: ProbabilisticScorer<Arc<NetworkGraph<&'a WrappedLog>>, &'a WrappedLog>,
548+
/// The pathfinder implementation to use for finding routes
549+
pathfinder: P,
502550
}
503551

504-
impl<'a, T: SimNetwork> SimNode<'a, T> {
552+
impl<'a, T: SimNetwork, P: PathFinder<'a>> SimNode<'a, T, P> {
505553
/// Creates a new simulation node that refers to the high level network coordinator provided to process payments
506554
/// on its behalf. The pathfinding graph is provided separately so that each node can handle its own pathfinding.
507555
pub fn new(
508556
pubkey: PublicKey,
509557
payment_network: Arc<Mutex<T>>,
510558
pathfinding_graph: Arc<NetworkGraph<&'a WrappedLog>>,
559+
pathfinder: P,
511560
) -> Self {
512561
// Initialize the probabilistic scorer with default parameters for learning from payment
513562
// history. These parameters control how much successful/failed payments affect routing
@@ -524,6 +573,7 @@ impl<'a, T: SimNetwork> SimNode<'a, T> {
524573
in_flight: HashMap::new(),
525574
pathfinding_graph,
526575
scorer,
576+
pathfinder,
527577
}
528578
}
529579

@@ -557,6 +607,37 @@ impl<'a, T: SimNetwork> SimNode<'a, T> {
557607

558608
Ok(())
559609
}
610+
611+
/// Dispatches a payment to a specified route.
612+
/// The [`lightning::routing::router::build_route_from_hops`] function can be used to build the route to be passed here.
613+
///
614+
/// **Note:** The payment hash passed in here should be used in track_payment to track the payment outcome.
615+
pub async fn send_to_route(
616+
&mut self,
617+
route: Route,
618+
payment_hash: PaymentHash,
619+
) -> Result<(), LightningError> {
620+
let (sender, receiver) = channel();
621+
622+
// Check for payment hash collision, failing the payment if we happen to repeat one.
623+
match self.in_flight.entry(payment_hash) {
624+
Entry::Occupied(_) => {
625+
return Err(LightningError::SendPaymentError(
626+
"payment hash exists".to_string(),
627+
));
628+
},
629+
Entry::Vacant(vacant) => {
630+
vacant.insert(receiver);
631+
},
632+
}
633+
634+
self.network
635+
.lock()
636+
.await
637+
.dispatch_payment(self.info.pubkey, route, payment_hash, sender);
638+
639+
Ok(())
640+
}
560641
}
561642

562643
/// Produces the node info for a mocked node, filling in the features that the simulator requires.
@@ -572,39 +653,8 @@ fn node_info(pubkey: PublicKey) -> NodeInfo {
572653
}
573654
}
574655

575-
/// Uses LDK's pathfinding algorithm with default parameters to find a path from source to destination, with no
576-
/// restrictions on fee budget.
577-
fn find_payment_route<'a>(
578-
source: &PublicKey,
579-
dest: PublicKey,
580-
amount_msat: u64,
581-
pathfinding_graph: &NetworkGraph<&'a WrappedLog>,
582-
scorer: &ProbabilisticScorer<Arc<NetworkGraph<&'a WrappedLog>>, &'a WrappedLog>,
583-
) -> Result<Route, SimulationError> {
584-
find_route(
585-
source,
586-
&RouteParameters {
587-
payment_params: PaymentParameters::from_node_id(dest, 0)
588-
.with_max_total_cltv_expiry_delta(u32::MAX)
589-
// TODO: set non-zero value to support MPP.
590-
.with_max_path_count(1)
591-
// Allow sending htlcs up to 50% of the channel's capacity.
592-
.with_max_channel_saturation_power_of_half(1),
593-
final_value_msat: amount_msat,
594-
max_total_routing_fee_msat: None,
595-
},
596-
pathfinding_graph,
597-
None,
598-
&WrappedLog {},
599-
scorer,
600-
&Default::default(),
601-
&[0; 32],
602-
)
603-
.map_err(|e| SimulationError::SimulatedNetworkError(e.err))
604-
}
605-
606656
#[async_trait]
607-
impl<T: SimNetwork> LightningNode for SimNode<'_, T> {
657+
impl<'a, T: SimNetwork, P: PathFinder<'a>> LightningNode for SimNode<'a, T, P> {
608658
fn get_info(&self) -> &NodeInfo {
609659
&self.info
610660
}
@@ -620,8 +670,24 @@ impl<T: SimNetwork> LightningNode for SimNode<'_, T> {
620670
dest: PublicKey,
621671
amount_msat: u64,
622672
) -> Result<PaymentHash, LightningError> {
623-
// Create a sender and receiver pair that will be used to report the results of the payment and add them to
624-
// our internal tracking state along with the chosen payment hash.
673+
// Use the stored scorer when finding a route
674+
let route = match self.pathfinder.find_route(
675+
&self.info.pubkey,
676+
dest,
677+
amount_msat,
678+
&self.pathfinding_graph,
679+
&self.scorer,
680+
) {
681+
Ok(route) => route,
682+
Err(e) => {
683+
log::warn!("No route found: {e}");
684+
return Err(LightningError::SendPaymentError(format!(
685+
"No route found: {e}"
686+
)));
687+
},
688+
};
689+
690+
// Create a channel to receive the payment result.
625691
let (sender, receiver) = channel();
626692
let preimage = PaymentPreimage(rand::random());
627693
let payment_hash = preimage.into();
@@ -638,36 +704,13 @@ impl<T: SimNetwork> LightningNode for SimNode<'_, T> {
638704
},
639705
}
640706

641-
// Use the stored scorer when finding a route
642-
let route = match find_payment_route(
643-
&self.info.pubkey,
644-
dest,
645-
amount_msat,
646-
&self.pathfinding_graph,
647-
&self.scorer,
648-
) {
649-
Ok(path) => path,
650-
// In the case that we can't find a route for the payment, we still report a successful payment *api call*
651-
// and report RouteNotFound to the tracking channel. This mimics the behavior of real nodes.
652-
Err(e) => {
653-
log::trace!("Could not find path for payment: {:?}.", e);
654-
655-
if let Err(e) = sender.send(Ok(PaymentResult {
656-
htlc_count: 0,
657-
payment_outcome: PaymentOutcome::RouteNotFound,
658-
})) {
659-
log::error!("Could not send payment result: {:?}.", e);
660-
}
661-
662-
return Ok(payment_hash);
663-
},
664-
};
665-
666-
// If we did successfully obtain a route, dispatch the payment through the network and then report success.
667-
self.network
668-
.lock()
669-
.await
670-
.dispatch_payment(self.info.pubkey, route, payment_hash, sender);
707+
// Dispatch the payment through the network
708+
self.network.lock().await.dispatch_payment(
709+
self.info.pubkey,
710+
route,
711+
payment_hash,
712+
sender,
713+
);
671714

672715
Ok(payment_hash)
673716
}
@@ -1007,19 +1050,24 @@ impl SimGraph {
10071050
}
10081051

10091052
/// Produces a map of node public key to lightning node implementation to be used for simulations.
1010-
pub async fn ln_node_from_graph(
1053+
pub async fn ln_node_from_graph<P>(
10111054
graph: Arc<Mutex<SimGraph>>,
1012-
routing_graph: Arc<NetworkGraph<&'_ WrappedLog>>,
1013-
) -> HashMap<PublicKey, Arc<Mutex<dyn LightningNode + '_>>> {
1055+
routing_graph: Arc<NetworkGraph<&'static WrappedLog>>,
1056+
pathfinder: P,
1057+
) -> HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>>
1058+
where
1059+
P: for<'a> PathFinder<'a> + Clone + 'static,
1060+
{
10141061
let mut nodes: HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>> = HashMap::new();
1015-
1062+
10161063
for pk in graph.lock().await.nodes.keys() {
10171064
nodes.insert(
10181065
*pk,
10191066
Arc::new(Mutex::new(SimNode::new(
10201067
*pk,
10211068
graph.clone(),
10221069
routing_graph.clone(),
1070+
pathfinder.clone(),
10231071
))),
10241072
);
10251073
}
@@ -1897,7 +1945,7 @@ mod tests {
18971945

18981946
// Create a simulated node for the first channel in our network.
18991947
let pk = channels[0].node_1.policy.pubkey;
1900-
let mut node = SimNode::new(pk, sim_network.clone(), Arc::new(graph));
1948+
let mut node = SimNode::new(pk, sim_network.clone(), Arc::new(graph), DefaultPathFinder);
19011949

19021950
// Prime mock to return node info from lookup and assert that we get the pubkey we're expecting.
19031951
let lookup_pk = channels[3].node_1.policy.pubkey;
@@ -1988,6 +2036,7 @@ mod tests {
19882036
routing_graph: Arc<NetworkGraph<&'a WrappedLog>>,
19892037
scorer: ProbabilisticScorer<Arc<NetworkGraph<&'a WrappedLog>>, &'a WrappedLog>,
19902038
shutdown: (Trigger, Listener),
2039+
pathfinder: DefaultPathFinder,
19912040
}
19922041

19932042
impl DispatchPaymentTestKit<'_> {
@@ -2036,6 +2085,7 @@ mod tests {
20362085
routing_graph,
20372086
scorer,
20382087
shutdown: shutdown_clone,
2088+
pathfinder: DefaultPathFinder,
20392089
};
20402090

20412091
// Assert that our channel balance is all on the side of the channel opener when we start up.
@@ -2078,8 +2128,13 @@ mod tests {
20782128
dest: PublicKey,
20792129
amt: u64,
20802130
) -> (Route, Result<PaymentResult, LightningError>) {
2081-
let route =
2082-
find_payment_route(&source, dest, amt, &self.routing_graph, &self.scorer).unwrap();
2131+
let route = self.pathfinder.find_route(
2132+
&source,
2133+
dest,
2134+
amt,
2135+
&self.routing_graph,
2136+
&self.scorer,
2137+
).unwrap();
20832138

20842139
let (sender, receiver) = oneshot::channel();
20852140
self.graph

0 commit comments

Comments
 (0)