Skip to content

Commit a6e7c87

Browse files
sangbidachuksys
authored andcommitted
Add tests
1 parent e8bfcf4 commit a6e7c87

File tree

6 files changed

+277
-12
lines changed

6 files changed

+277
-12
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: 225 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,6 @@ pub async fn create_simulation_with_network<P: for<'a> PathFinder<'a> + Clone +
361361
))
362362
}
363363

364-
365364
/// Parses the cli options provided and creates a simulation to be run, connecting to lightning nodes and validating
366365
/// any activity described in the simulation file.
367366
pub async fn create_simulation(
@@ -659,3 +658,228 @@ pub async fn get_validated_activities(
659658

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

simln-lib/src/sim_node.rs

Lines changed: 15 additions & 8 deletions
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 {
@@ -1139,7 +1149,7 @@ where
11391149
P: for<'a> PathFinder<'a> + Clone + 'static,
11401150
{
11411151
let mut nodes: HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>> = HashMap::new();
1142-
1152+
11431153
for pk in graph.lock().await.nodes.keys() {
11441154
nodes.insert(
11451155
*node.0,
@@ -2288,13 +2298,10 @@ mod tests {
22882298
dest: PublicKey,
22892299
amt: u64,
22902300
) -> (Route, Result<PaymentResult, LightningError>) {
2291-
let route = self.pathfinder.find_route(
2292-
&source,
2293-
dest,
2294-
amt,
2295-
&self.routing_graph,
2296-
&self.scorer,
2297-
).unwrap();
2301+
let route = self
2302+
.pathfinder
2303+
.find_route(&source, dest, amt, &self.routing_graph, &self.scorer)
2304+
.unwrap();
22982305

22992306
let (sender, receiver) = oneshot::channel();
23002307
self.graph

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)