@@ -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.
367366pub 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+ }
0 commit comments