Skip to content

Commit 8c43804

Browse files
sangbidachuksys
authored andcommitted
simln-lib/test: Add tests for custom pathfinding
1 parent 4a74f9f commit 8c43804

File tree

6 files changed

+277
-5
lines changed

6 files changed

+277
-5
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sim-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ futures = "0.3.30"
2828
console-subscriber = { version = "0.4.0", optional = true}
2929
tokio-util = { version = "0.7.13", features = ["rt"] }
3030
openssl = { version = "0.10", features = ["vendored"] }
31+
lightning = { version = "0.0.123" }
3132

3233
[features]
3334
dev = ["console-subscriber"]

sim-cli/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async fn main() -> anyhow::Result<()> {
3636
cli.validate(&sim_params)?;
3737

3838
let tasks = TaskTracker::new();
39-
39+
4040
// Create the pathfinder instance
4141
let pathfinder = DefaultPathFinder;
4242

@@ -57,8 +57,8 @@ async fn main() -> anyhow::Result<()> {
5757
clock,
5858
tasks.clone(),
5959
interceptors,
60-
pathfinder,
6160
CustomRecords::default(),
61+
pathfinder,
6262
)
6363
.await?;
6464
(sim, validated_activities)
@@ -73,4 +73,4 @@ async fn main() -> anyhow::Result<()> {
7373
sim.run(&validated_activities).await?;
7474

7575
Ok(())
76-
}
76+
}

sim-cli/src/parsing.rs

Lines changed: 229 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,6 @@ pub async fn create_simulation_with_network(
339339
))
340340
}
341341

342-
343342
/// Parses the cli options provided and creates a simulation to be run, connecting to lightning nodes and validating
344343
/// any activity described in the simulation file.
345344
pub async fn create_simulation(
@@ -637,3 +636,232 @@ pub async fn get_validated_activities(
637636

638637
validate_activities(activity.to_vec(), activity_validation_params).await
639638
}
639+
640+
#[cfg(test)]
641+
mod tests {
642+
use super::*;
643+
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
644+
use lightning::routing::gossip::NetworkGraph;
645+
use lightning::routing::router::{find_route, PaymentParameters, Route, RouteParameters};
646+
use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringDecayParameters};
647+
use rand::RngCore;
648+
use simln_lib::clock::SystemClock;
649+
use simln_lib::sim_node::{
650+
ln_node_from_graph, populate_network_graph, PathFinder, SimGraph, WrappedLog,
651+
};
652+
use simln_lib::SimulationError;
653+
use std::sync::Arc;
654+
use tokio::sync::Mutex;
655+
use tokio_util::task::TaskTracker;
656+
657+
/// Gets a key pair generated in a pseudorandom way.
658+
fn get_random_keypair() -> (SecretKey, PublicKey) {
659+
let secp = Secp256k1::new();
660+
let mut rng = rand::thread_rng();
661+
let mut bytes = [0u8; 32];
662+
rng.fill_bytes(&mut bytes);
663+
let secret_key = SecretKey::from_slice(&bytes).expect("Failed to create secret key");
664+
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
665+
(secret_key, public_key)
666+
}
667+
668+
/// Helper function to create simulated channels for testing
669+
fn create_simulated_channels(num_channels: usize, capacity_msat: u64) -> Vec<SimulatedChannel> {
670+
let mut channels = Vec::new();
671+
for i in 0..num_channels {
672+
let (_node1_sk, node1_pubkey) = get_random_keypair();
673+
let (_node2_sk, node2_pubkey) = get_random_keypair();
674+
675+
let channel = SimulatedChannel::new(
676+
capacity_msat,
677+
ShortChannelID::from(i as u64),
678+
ChannelPolicy {
679+
pubkey: node1_pubkey,
680+
alias: "".to_string(),
681+
max_htlc_count: 483,
682+
max_in_flight_msat: capacity_msat / 2,
683+
min_htlc_size_msat: 1000,
684+
max_htlc_size_msat: capacity_msat / 2,
685+
cltv_expiry_delta: 144,
686+
base_fee: 1000,
687+
fee_rate_prop: 100,
688+
},
689+
ChannelPolicy {
690+
pubkey: node2_pubkey,
691+
alias: "".to_string(),
692+
max_htlc_count: 483,
693+
max_in_flight_msat: capacity_msat / 2,
694+
min_htlc_size_msat: 1000,
695+
max_htlc_size_msat: capacity_msat / 2,
696+
cltv_expiry_delta: 144,
697+
base_fee: 1000,
698+
fee_rate_prop: 100,
699+
},
700+
);
701+
channels.push(channel);
702+
}
703+
channels
704+
}
705+
706+
/// A pathfinder that always fails to find a path
707+
#[derive(Clone)]
708+
pub struct AlwaysFailPathFinder;
709+
710+
impl<'a> PathFinder<'a> for AlwaysFailPathFinder {
711+
fn find_route(
712+
&self,
713+
_source: &PublicKey,
714+
_dest: PublicKey,
715+
_amount_msat: u64,
716+
_pathfinding_graph: &NetworkGraph<&'a WrappedLog>,
717+
_scorer: &ProbabilisticScorer<Arc<NetworkGraph<&'a WrappedLog>>, &'a WrappedLog>,
718+
) -> Result<Route, SimulationError> {
719+
Err(SimulationError::SimulatedNetworkError(
720+
"No route found".to_string(),
721+
))
722+
}
723+
}
724+
725+
/// A pathfinder that only returns single-hop paths
726+
#[derive(Clone)]
727+
pub struct SingleHopOnlyPathFinder;
728+
729+
impl<'a> PathFinder<'a> for SingleHopOnlyPathFinder {
730+
fn find_route(
731+
&self,
732+
source: &PublicKey,
733+
dest: PublicKey,
734+
amount_msat: u64,
735+
pathfinding_graph: &NetworkGraph<&'a WrappedLog>,
736+
scorer: &ProbabilisticScorer<Arc<NetworkGraph<&'a WrappedLog>>, &'a WrappedLog>,
737+
) -> Result<Route, SimulationError> {
738+
// Try to find a direct route only (single hop)
739+
let route_params = RouteParameters {
740+
payment_params: PaymentParameters::from_node_id(dest, 0)
741+
.with_max_total_cltv_expiry_delta(u32::MAX)
742+
.with_max_path_count(1)
743+
.with_max_channel_saturation_power_of_half(1),
744+
final_value_msat: amount_msat,
745+
max_total_routing_fee_msat: None,
746+
};
747+
748+
// Try to find a route - if it fails or has more than one hop, return an error
749+
match find_route(
750+
source,
751+
&route_params,
752+
pathfinding_graph,
753+
None,
754+
&WrappedLog {},
755+
scorer,
756+
&Default::default(),
757+
&[0; 32],
758+
) {
759+
Ok(route) => {
760+
// Check if the route has exactly one hop
761+
if route.paths.len() == 1 && route.paths[0].hops.len() == 1 {
762+
Ok(route)
763+
} else {
764+
Err(SimulationError::SimulatedNetworkError(
765+
"No direct route found".to_string(),
766+
))
767+
}
768+
},
769+
Err(e) => Err(SimulationError::SimulatedNetworkError(e.err)),
770+
}
771+
}
772+
}
773+
774+
#[tokio::test]
775+
async fn test_always_fail_pathfinder() {
776+
let channels = create_simulated_channels(3, 1_000_000_000);
777+
let routing_graph =
778+
Arc::new(populate_network_graph(channels.clone(), Arc::new(SystemClock {})).unwrap());
779+
780+
let pathfinder = AlwaysFailPathFinder;
781+
let source = channels[0].get_node_1_pubkey();
782+
let dest = channels[2].get_node_2_pubkey();
783+
784+
let scorer = ProbabilisticScorer::new(
785+
ProbabilisticScoringDecayParameters::default(),
786+
routing_graph.clone(),
787+
&WrappedLog {},
788+
);
789+
790+
let result = pathfinder.find_route(&source, dest, 100_000, &routing_graph,);
791+
792+
// Should always fail
793+
assert!(result.is_err());
794+
}
795+
796+
#[tokio::test]
797+
async fn test_single_hop_only_pathfinder() {
798+
let channels = create_simulated_channels(3, 1_000_000_000);
799+
let routing_graph =
800+
Arc::new(populate_network_graph(channels.clone(), Arc::new(SystemClock {})).unwrap());
801+
802+
let pathfinder = SingleHopOnlyPathFinder;
803+
let source = channels[0].get_node_1_pubkey();
804+
805+
let scorer = ProbabilisticScorer::new(
806+
ProbabilisticScoringDecayParameters::default(),
807+
routing_graph.clone(),
808+
&WrappedLog {},
809+
);
810+
811+
// Test direct connection (should work)
812+
let direct_dest = channels[0].get_node_2_pubkey();
813+
let result = pathfinder.find_route(&source, direct_dest, 100_000, &routing_graph,);
814+
815+
if result.is_ok() {
816+
let route = result.unwrap();
817+
assert_eq!(route.paths[0].hops.len(), 1); // Only one hop
818+
}
819+
820+
// Test indirect connection (should fail)
821+
let indirect_dest = channels[2].get_node_2_pubkey();
822+
let _result =
823+
pathfinder.find_route(&source, indirect_dest, 100_000, &routing_graph,);
824+
825+
// May fail because no direct route exists
826+
// (depends on your test network topology)
827+
}
828+
829+
/// Test that different pathfinders produce different behavior in payments
830+
#[tokio::test]
831+
async fn test_pathfinder_affects_payment_behavior() {
832+
let channels = create_simulated_channels(3, 1_000_000_000);
833+
let (shutdown_trigger, shutdown_listener) = triggered::trigger();
834+
let sim_graph = Arc::new(Mutex::new(
835+
SimGraph::new(
836+
channels.clone(),
837+
TaskTracker::new(),
838+
Vec::new(),
839+
HashMap::new(), // Empty custom records
840+
(shutdown_trigger.clone(), shutdown_listener.clone()),
841+
)
842+
.unwrap(),
843+
));
844+
let routing_graph =
845+
Arc::new(populate_network_graph(channels.clone(), Arc::new(SystemClock {})).unwrap());
846+
847+
// Create nodes with different pathfinders
848+
let nodes_default = ln_node_from_graph(
849+
sim_graph.clone(),
850+
routing_graph.clone(),
851+
SystemClock {},
852+
simln_lib::sim_node::DefaultPathFinder,
853+
)
854+
.await;
855+
856+
let nodes_fail = ln_node_from_graph(
857+
sim_graph.clone(),
858+
routing_graph.clone(),
859+
SystemClock {},
860+
AlwaysFailPathFinder,
861+
)
862+
.await;
863+
864+
// Both should create the same structure
865+
assert_eq!(nodes_default.len(), nodes_fail.len());
866+
}
867+
}

simln-lib/src/sim_node.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,16 @@ impl SimulatedChannel {
338338
}
339339
}
340340

341+
/// Gets the public key of node 1 in the channel.
342+
pub fn get_node_1_pubkey(&self) -> PublicKey {
343+
self.node_1.policy.pubkey
344+
}
345+
346+
/// Gets the public key of node 2 in the channel.
347+
pub fn get_node_2_pubkey(&self) -> PublicKey {
348+
self.node_2.policy.pubkey
349+
}
350+
341351
/// Validates that a simulated channel has distinct node pairs and valid routing policies.
342352
fn validate(&self) -> Result<(), SimulationError> {
343353
if self.node_1.policy.pubkey == self.node_2.policy.pubkey {
@@ -1121,7 +1131,7 @@ where
11211131
P: PathFinder + 'static,
11221132
{
11231133
let mut nodes: HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>> = HashMap::new();
1124-
1134+
11251135
for pk in graph.lock().await.nodes.keys() {
11261136
nodes.insert(
11271137
*node.0,

simln-lib/src/test_utils.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,35 @@ pub fn create_activity(
250250
amount_msat: ValueOrRange::Value(amount_msat),
251251
}
252252
}
253+
254+
#[cfg(test)]
255+
mod tests {
256+
use super::*;
257+
258+
#[test]
259+
fn test_create_activity() {
260+
let (_source_sk, source_pk) = get_random_keypair();
261+
let (_dest_sk, dest_pk) = get_random_keypair();
262+
263+
let source_info = NodeInfo {
264+
pubkey: source_pk,
265+
alias: "source".to_string(),
266+
features: Features::empty(),
267+
};
268+
269+
let dest_info = NodeInfo {
270+
pubkey: dest_pk,
271+
alias: "destination".to_string(),
272+
features: Features::empty(),
273+
};
274+
275+
let activity = create_activity(source_info.clone(), dest_info.clone(), 1000);
276+
277+
assert_eq!(activity.source.pubkey, source_info.pubkey);
278+
assert_eq!(activity.destination.pubkey, dest_info.pubkey);
279+
match activity.amount_msat {
280+
ValueOrRange::Value(amount) => assert_eq!(amount, 1000),
281+
ValueOrRange::Range(_, _) => panic!("Expected Value variant, got Range"),
282+
}
283+
}
284+
}

0 commit comments

Comments
 (0)