@@ -644,7 +644,12 @@ struct ExecutorPaymentTracker {
644644
645645impl Ord for PaymentEvent {
646646 fn cmp ( & self , other : & Self ) -> std:: cmp:: Ordering {
647- self . execution_time . cmp ( & other. execution_time )
647+ // Order primarily by execution time, then break ties on the source node's public key. The heap only ever holds
648+ // at most one event per source at a time, so `(execution_time, source)` is a total order. Without the tie-break,
649+ // events scheduled for the same instant pop in an unspecified order, which makes runs non-reproducible.
650+ self . execution_time
651+ . cmp ( & other. execution_time )
652+ . then_with ( || self . source . cmp ( & other. source ) )
648653 }
649654}
650655
@@ -656,7 +661,7 @@ impl PartialOrd for PaymentEvent {
656661
657662impl PartialEq for PaymentEvent {
658663 fn eq ( & self , other : & Self ) -> bool {
659- self . execution_time == other. execution_time
664+ self . execution_time == other. execution_time && self . source == other . source
660665 }
661666}
662667
@@ -1190,12 +1195,14 @@ async fn produce_payment_events<C: Clock>(
11901195 _ = pe_clock. sleep( wait_time) => {
11911196 generate_payment( & mut heap, source, & mut payments_tracker, clock. now( ) ) . await ?;
11921197
1198+ // Stamp the dispatch time from the simulation clock now that the wait has elapsed.
1199+ let dispatch_time = pe_clock. now( ) ;
11931200 tasks. spawn( async move {
11941201 log:: debug!( "Generated payment: {source} -> {}: {amount} msat." , destination) ;
11951202
11961203 // Send the payment, exiting if we can no longer send to the consumer.
11971204 let event = SimulationEvent :: SendPayment ( destination. clone( ) , amount) ;
1198- if let Err ( e) = send_payment( node, pe_output_sender, event. clone( ) ) . await {
1205+ if let Err ( e) = send_payment( node, pe_output_sender, event. clone( ) , dispatch_time ) . await {
11991206 pe_shutdown. trigger( ) ;
12001207 log:: debug!( "Not able to send event payment for {amount}: {source} -> {}. Exited with error {e}." , destination) ;
12011208 } else {
@@ -1270,6 +1277,7 @@ async fn send_payment(
12701277 node : Arc < Mutex < dyn LightningNode > > ,
12711278 sender : Sender < SimulationOutput > ,
12721279 simulation_event : SimulationEvent ,
1280+ dispatch_time : SystemTime ,
12731281) -> Result < ( ) , SimulationError > {
12741282 match simulation_event {
12751283 SimulationEvent :: SendPayment ( dest, amt_msat) => {
@@ -1280,7 +1288,9 @@ async fn send_payment(
12801288 hash : None ,
12811289 amount_msat : amt_msat,
12821290 destination : dest. pubkey ,
1283- dispatch_time : SystemTime :: now ( ) ,
1291+ // Take the dispatch time from the simulation clock rather than the wall clock, so that it advances with
1292+ // virtual time under discrete-event simulation and stays reproducible across runs.
1293+ dispatch_time,
12841294 } ;
12851295
12861296 let outcome = match node. send_payment ( dest. pubkey , amt_msat) . await {
@@ -1509,6 +1519,7 @@ async fn produce_simulation_results(
15091519 } ,
15101520 SimulationOutput :: SendPaymentFailure ( payment, result) => {
15111521 select ! {
1522+ biased;
15121523 _ = listener. clone( ) => {
15131524 return Ok ( ( ) ) ;
15141525 } ,
@@ -1669,6 +1680,39 @@ mod tests {
16691680 assert_eq ! ( seq1, seq1_again) ;
16701681 }
16711682
1683+ #[ test]
1684+ fn test_payment_event_orders_ties_by_source ( ) {
1685+ use crate :: PaymentEvent ;
1686+ use std:: cmp:: Reverse ;
1687+ use std:: collections:: BinaryHeap ;
1688+ use std:: time:: { Duration , SystemTime } ;
1689+
1690+ let nodes = test_utils:: create_nodes ( 2 , 100_000 ) ;
1691+ let ( mut low, mut high) = ( nodes[ 0 ] . 0 . clone ( ) , nodes[ 1 ] . 0 . clone ( ) ) ;
1692+ // Order our two nodes so that `low` has the smaller public key.
1693+ if low. pubkey > high. pubkey {
1694+ std:: mem:: swap ( & mut low, & mut high) ;
1695+ }
1696+
1697+ let when = SystemTime :: UNIX_EPOCH + Duration :: from_secs ( 10 ) ;
1698+ let event = |source : & NodeInfo , destination : & NodeInfo | PaymentEvent {
1699+ source : source. pubkey ,
1700+ execution_time : when,
1701+ destination : destination. clone ( ) ,
1702+ amount : 1_000 ,
1703+ } ;
1704+
1705+ // Push the higher-keyed source first to prove that pop order is decided by the tie-break, not by
1706+ // insertion order.
1707+ let mut heap: BinaryHeap < Reverse < PaymentEvent > > = BinaryHeap :: new ( ) ;
1708+ heap. push ( Reverse ( event ( & high, & low) ) ) ;
1709+ heap. push ( Reverse ( event ( & low, & high) ) ) ;
1710+
1711+ // Both events share an execution time, so the min-heap must pop the smaller public key first.
1712+ assert_eq ! ( heap. pop( ) . unwrap( ) . 0 . source, low. pubkey) ;
1713+ assert_eq ! ( heap. pop( ) . unwrap( ) . 0 . source, high. pubkey) ;
1714+ }
1715+
16721716 mock ! {
16731717 pub Generator { }
16741718
0 commit comments