Skip to content

Commit 2dc39e5

Browse files
committed
Add tests
1 parent 17e0cb6 commit 2dc39e5

File tree

6 files changed

+282
-19
lines changed

6 files changed

+282
-19
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
@@ -34,7 +34,7 @@ async fn main() -> anyhow::Result<()> {
3434
cli.validate(&sim_params)?;
3535

3636
let tasks = TaskTracker::new();
37-
37+
3838
// Create the pathfinder instance
3939
let pathfinder = DefaultPathFinder;
4040

@@ -52,8 +52,8 @@ async fn main() -> anyhow::Result<()> {
5252
&sim_params,
5353
tasks.clone(),
5454
interceptors,
55-
pathfinder,
5655
CustomRecords::default(),
56+
pathfinder,
5757
)
5858
.await?
5959
};
@@ -67,4 +67,4 @@ async fn main() -> anyhow::Result<()> {
6767
sim.run(&validated_activities).await?;
6868

6969
Ok(())
70-
}
70+
}

sim-cli/src/parsing.rs

Lines changed: 225 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,6 @@ pub async fn create_simulation_with_network<P: for<'a> PathFinder<'a> + Clone +
296296
))
297297
}
298298

299-
300299
/// Parses the cli options provided and creates a simulation to be run, connecting to lightning nodes and validating
301300
/// any activity described in the simulation file.
302301
pub async fn create_simulation(
@@ -593,3 +592,228 @@ pub async fn get_validated_activities(
593592

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

simln-lib/src/sim_node.rs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,16 @@ impl SimulatedChannel {
334334
}
335335
}
336336

337+
/// Gets the public key of node 1 in the channel.
338+
pub fn get_node_1_pubkey(&self) -> PublicKey {
339+
self.node_1.policy.pubkey
340+
}
341+
342+
/// Gets the public key of node 2 in the channel.
343+
pub fn get_node_2_pubkey(&self) -> PublicKey {
344+
self.node_2.policy.pubkey
345+
}
346+
337347
/// Validates that a simulated channel has distinct node pairs and valid routing policies.
338348
fn validate(&self) -> Result<(), SimulationError> {
339349
if self.node_1.policy.pubkey == self.node_2.policy.pubkey {
@@ -643,12 +653,10 @@ impl<'a, T: SimNetwork, P: PathFinder<'a>> LightningNode for SimNode<'a, T, P> {
643653
}
644654

645655
// Dispatch the payment through the network
646-
self.network.lock().await.dispatch_payment(
647-
self.info.pubkey,
648-
route,
649-
payment_hash,
650-
sender,
651-
);
656+
self.network
657+
.lock()
658+
.await
659+
.dispatch_payment(self.info.pubkey, route, payment_hash, sender);
652660

653661
Ok(payment_hash)
654662
}
@@ -991,12 +999,12 @@ pub async fn ln_node_from_graph<P>(
991999
graph: Arc<Mutex<SimGraph>>,
9921000
routing_graph: Arc<NetworkGraph<&'static WrappedLog>>,
9931001
pathfinder: P,
994-
) -> HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>>
1002+
) -> HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>>
9951003
where
9961004
P: for<'a> PathFinder<'a> + Clone + 'static,
9971005
{
9981006
let mut nodes: HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>> = HashMap::new();
999-
1007+
10001008
for pk in graph.lock().await.nodes.keys() {
10011009
nodes.insert(
10021010
*pk,
@@ -2064,13 +2072,10 @@ mod tests {
20642072
dest: PublicKey,
20652073
amt: u64,
20662074
) -> (Route, Result<PaymentResult, LightningError>) {
2067-
let route = self.pathfinder.find_route(
2068-
&source,
2069-
dest,
2070-
amt,
2071-
&self.routing_graph,
2072-
&self.scorer,
2073-
).unwrap();
2075+
let route = self
2076+
.pathfinder
2077+
.find_route(&source, dest, amt, &self.routing_graph, &self.scorer)
2078+
.unwrap();
20742079

20752080
let (sender, receiver) = oneshot::channel();
20762081
self.graph

simln-lib/src/test_utils.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,35 @@ pub fn create_activity(
224224
amount_msat: ValueOrRange::Value(amount_msat),
225225
}
226226
}
227+
228+
#[cfg(test)]
229+
mod tests {
230+
use super::*;
231+
232+
#[test]
233+
fn test_create_activity() {
234+
let (_source_sk, source_pk) = get_random_keypair();
235+
let (_dest_sk, dest_pk) = get_random_keypair();
236+
237+
let source_info = NodeInfo {
238+
pubkey: source_pk,
239+
alias: "source".to_string(),
240+
features: Features::empty(),
241+
};
242+
243+
let dest_info = NodeInfo {
244+
pubkey: dest_pk,
245+
alias: "destination".to_string(),
246+
features: Features::empty(),
247+
};
248+
249+
let activity = create_activity(source_info.clone(), dest_info.clone(), 1000);
250+
251+
assert_eq!(activity.source.pubkey, source_info.pubkey);
252+
assert_eq!(activity.destination.pubkey, dest_info.pubkey);
253+
match activity.amount_msat {
254+
ValueOrRange::Value(amount) => assert_eq!(amount, 1000),
255+
ValueOrRange::Range(_, _) => panic!("Expected Value variant, got Range"),
256+
}
257+
}
258+
}

0 commit comments

Comments
 (0)