@@ -200,6 +200,17 @@ struct PathBuildingHop {
200
200
/// A mirror of the same field in RouteGraphNode. Note that this is only used during the graph
201
201
/// walk and may be invalid thereafter.
202
202
path_htlc_minimum_msat : u64 ,
203
+ /// If we've already processed a node as the best node, we shouldn't process it again. Normally
204
+ /// we'd just ignore it if we did as all channels would have a higher new fee, but because we
205
+ /// may decrease the amounts in use as we walk the graph, the actual calculated fee may
206
+ /// decrease as well. Thus, we have to explicitly track which nodes have been processed and
207
+ /// avoid processing them again.
208
+ was_processed : bool ,
209
+ #[ cfg( test) ]
210
+ // In tests, we apply further sanity checks on cases where we skip nodes we already processed
211
+ // to ensure it is specifically in cases where the fee has gone down because of a decrease in
212
+ // valud_contribution_msat, which requires tracking it here.
213
+ value_contribution_msat : u64 ,
203
214
}
204
215
205
216
// Instantiated with a list of hops with correct data in them collected during path finding,
@@ -615,87 +626,116 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
615
626
total_fee_msat: u64 :: max_value( ) ,
616
627
htlc_minimum_msat: $directional_info. htlc_minimum_msat,
617
628
path_htlc_minimum_msat,
629
+ was_processed: false ,
630
+ #[ cfg( test) ]
631
+ value_contribution_msat,
618
632
}
619
633
} ) ;
620
634
621
- let mut hop_use_fee_msat = 0 ;
622
- let mut total_fee_msat = $next_hops_fee_msat;
623
-
624
- // Ignore hop_use_fee_msat for channel-from-us as we assume all channels-from-us
625
- // will have the same effective-fee
626
- if $src_node_id != * our_node_id {
627
- match compute_fees( amount_to_transfer_over_msat, $directional_info. fees) {
628
- // max_value means we'll always fail
629
- // the old_entry.total_fee_msat > total_fee_msat check
630
- None => total_fee_msat = u64 :: max_value( ) ,
631
- Some ( fee_msat) => {
632
- hop_use_fee_msat = fee_msat;
633
- total_fee_msat += hop_use_fee_msat;
634
- // When calculating the lowest inbound fees to a node, we
635
- // calculate fees here not based on the actual value we think
636
- // will flow over this channel, but on the minimum value that
637
- // we'll accept flowing over it. The minimum accepted value
638
- // is a constant through each path collection run, ensuring
639
- // consistent basis. Otherwise we may later find a
640
- // different path to the source node that is more expensive,
641
- // but which we consider to be cheaper because we are capacity
642
- // constrained and the relative fee becomes lower.
643
- match compute_fees( minimal_value_contribution_msat, old_entry. src_lowest_inbound_fees)
644
- . map( |a| a. checked_add( total_fee_msat) ) {
645
- Some ( Some ( v) ) => {
646
- total_fee_msat = v;
647
- } ,
648
- _ => {
649
- total_fee_msat = u64 :: max_value( ) ;
650
- }
651
- } ;
652
- }
653
- }
635
+ #[ allow( unused_mut) ] // We only use the mut in cfg(test)
636
+ let mut should_process = !old_entry. was_processed;
637
+ #[ cfg( any( test, feature = "fuzztarget" ) ) ]
638
+ {
639
+ // In test/fuzzing builds, we do extra checks to make sure the skipping
640
+ // of already-seen nodes only happens in cases we expect (see below).
641
+ if !should_process { should_process = true ; }
654
642
}
655
643
656
- let new_graph_node = RouteGraphNode {
657
- pubkey: $src_node_id,
658
- lowest_fee_to_peer_through_node: total_fee_msat,
659
- lowest_fee_to_node: $next_hops_fee_msat as u64 + hop_use_fee_msat,
660
- value_contribution_msat: value_contribution_msat,
661
- path_htlc_minimum_msat,
662
- } ;
644
+ if should_process {
645
+ let mut hop_use_fee_msat = 0 ;
646
+ let mut total_fee_msat = $next_hops_fee_msat;
647
+
648
+ // Ignore hop_use_fee_msat for channel-from-us as we assume all channels-from-us
649
+ // will have the same effective-fee
650
+ if $src_node_id != * our_node_id {
651
+ match compute_fees( amount_to_transfer_over_msat, $directional_info. fees) {
652
+ // max_value means we'll always fail
653
+ // the old_entry.total_fee_msat > total_fee_msat check
654
+ None => total_fee_msat = u64 :: max_value( ) ,
655
+ Some ( fee_msat) => {
656
+ hop_use_fee_msat = fee_msat;
657
+ total_fee_msat += hop_use_fee_msat;
658
+ // When calculating the lowest inbound fees to a node, we
659
+ // calculate fees here not based on the actual value we think
660
+ // will flow over this channel, but on the minimum value that
661
+ // we'll accept flowing over it. The minimum accepted value
662
+ // is a constant through each path collection run, ensuring
663
+ // consistent basis. Otherwise we may later find a
664
+ // different path to the source node that is more expensive,
665
+ // but which we consider to be cheaper because we are capacity
666
+ // constrained and the relative fee becomes lower.
667
+ match compute_fees( minimal_value_contribution_msat, old_entry. src_lowest_inbound_fees)
668
+ . map( |a| a. checked_add( total_fee_msat) ) {
669
+ Some ( Some ( v) ) => {
670
+ total_fee_msat = v;
671
+ } ,
672
+ _ => {
673
+ total_fee_msat = u64 :: max_value( ) ;
674
+ }
675
+ } ;
676
+ }
677
+ }
678
+ }
663
679
664
- // Update the way of reaching $src_node_id with the given $chan_id (from $dest_node_id),
665
- // if this way is cheaper than the already known
666
- // (considering the cost to "reach" this channel from the route destination,
667
- // the cost of using this channel,
668
- // and the cost of routing to the source node of this channel).
669
- // Also, consider that htlc_minimum_msat_difference, because we might end up
670
- // paying it. Consider the following exploit:
671
- // we use 2 paths to transfer 1.5 BTC. One of them is 0-fee normal 1 BTC path,
672
- // and for the other one we picked a 1sat-fee path with htlc_minimum_msat of
673
- // 1 BTC. Now, since the latter is more expensive, we gonna try to cut it
674
- // by 0.5 BTC, but then match htlc_minimum_msat by paying a fee of 0.5 BTC
675
- // to this channel.
676
- // Ideally the scoring could be smarter (e.g. 0.5*htlc_minimum_msat here),
677
- // but it may require additional tracking - we don't want to double-count
678
- // the fees included in $next_hops_path_htlc_minimum_msat, but also
679
- // can't use something that may decrease on future hops.
680
- let old_cost = cmp:: max( old_entry. total_fee_msat, old_entry. path_htlc_minimum_msat) ;
681
- let new_cost = cmp:: max( total_fee_msat, path_htlc_minimum_msat) ;
682
-
683
- if new_cost < old_cost {
684
- targets. push( new_graph_node) ;
685
- old_entry. next_hops_fee_msat = $next_hops_fee_msat;
686
- old_entry. hop_use_fee_msat = hop_use_fee_msat;
687
- old_entry. total_fee_msat = total_fee_msat;
688
- old_entry. route_hop = RouteHop {
689
- pubkey: $dest_node_id. clone( ) ,
690
- node_features: NodeFeatures :: empty( ) ,
691
- short_channel_id: $chan_id. clone( ) ,
692
- channel_features: $chan_features. clone( ) ,
693
- fee_msat: 0 , // This value will be later filled with hop_use_fee_msat of the following channel
694
- cltv_expiry_delta: $directional_info. cltv_expiry_delta as u32 ,
680
+ let new_graph_node = RouteGraphNode {
681
+ pubkey: $src_node_id,
682
+ lowest_fee_to_peer_through_node: total_fee_msat,
683
+ lowest_fee_to_node: $next_hops_fee_msat as u64 + hop_use_fee_msat,
684
+ value_contribution_msat: value_contribution_msat,
685
+ path_htlc_minimum_msat,
695
686
} ;
696
- old_entry. channel_fees = $directional_info. fees;
697
- old_entry. htlc_minimum_msat = $directional_info. htlc_minimum_msat;
698
- old_entry. path_htlc_minimum_msat = path_htlc_minimum_msat;
687
+
688
+ // Update the way of reaching $src_node_id with the given $chan_id (from $dest_node_id),
689
+ // if this way is cheaper than the already known
690
+ // (considering the cost to "reach" this channel from the route destination,
691
+ // the cost of using this channel,
692
+ // and the cost of routing to the source node of this channel).
693
+ // Also, consider that htlc_minimum_msat_difference, because we might end up
694
+ // paying it. Consider the following exploit:
695
+ // we use 2 paths to transfer 1.5 BTC. One of them is 0-fee normal 1 BTC path,
696
+ // and for the other one we picked a 1sat-fee path with htlc_minimum_msat of
697
+ // 1 BTC. Now, since the latter is more expensive, we gonna try to cut it
698
+ // by 0.5 BTC, but then match htlc_minimum_msat by paying a fee of 0.5 BTC
699
+ // to this channel.
700
+ // Ideally the scoring could be smarter (e.g. 0.5*htlc_minimum_msat here),
701
+ // but it may require additional tracking - we don't want to double-count
702
+ // the fees included in $next_hops_path_htlc_minimum_msat, but also
703
+ // can't use something that may decrease on future hops.
704
+ let old_cost = cmp:: max( old_entry. total_fee_msat, old_entry. path_htlc_minimum_msat) ;
705
+ let new_cost = cmp:: max( total_fee_msat, path_htlc_minimum_msat) ;
706
+
707
+ if !old_entry. was_processed && new_cost < old_cost {
708
+ targets. push( new_graph_node) ;
709
+ old_entry. next_hops_fee_msat = $next_hops_fee_msat;
710
+ old_entry. hop_use_fee_msat = hop_use_fee_msat;
711
+ old_entry. total_fee_msat = total_fee_msat;
712
+ old_entry. route_hop = RouteHop {
713
+ pubkey: $dest_node_id. clone( ) ,
714
+ node_features: NodeFeatures :: empty( ) ,
715
+ short_channel_id: $chan_id. clone( ) ,
716
+ channel_features: $chan_features. clone( ) ,
717
+ fee_msat: 0 , // This value will be later filled with hop_use_fee_msat of the following channel
718
+ cltv_expiry_delta: $directional_info. cltv_expiry_delta as u32 ,
719
+ } ;
720
+ old_entry. channel_fees = $directional_info. fees;
721
+ old_entry. htlc_minimum_msat = $directional_info. htlc_minimum_msat;
722
+ old_entry. path_htlc_minimum_msat = path_htlc_minimum_msat;
723
+ #[ cfg( test) ]
724
+ {
725
+ old_entry. value_contribution_msat = value_contribution_msat;
726
+ }
727
+ } else if old_entry. was_processed && new_cost < old_cost {
728
+ #[ cfg( test) ]
729
+ {
730
+ // If we're skipping processing a node which was previously
731
+ // processed even though we found another path to it with a
732
+ // cheaper fee, check that it was because the second path we
733
+ // found (which we are processing now) has a lower value
734
+ // contribution due to an HTLC minimum limit.
735
+ debug_assert!( path_htlc_minimum_msat < old_entry. path_htlc_minimum_msat) ;
736
+ debug_assert!( value_contribution_msat < old_entry. value_contribution_msat) ;
737
+ }
738
+ }
699
739
}
700
740
}
701
741
}
@@ -710,7 +750,19 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
710
750
// This data can later be helpful to optimize routing (pay lower fees).
711
751
macro_rules! add_entries_to_cheapest_to_target_node {
712
752
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr ) => {
713
- if first_hops. is_some( ) {
753
+ let skip_node = if let Some ( elem) = dist. get_mut( $node_id) {
754
+ let v = elem. was_processed;
755
+ elem. was_processed = true ;
756
+ v
757
+ } else {
758
+ // Entries are added to dist in add_entry!() when there is a channel from a node.
759
+ // Because there are no channels from payee, it will not have a dist entry at this point.
760
+ // If we're processing any other node, it is always be the result of a channel from it.
761
+ assert_eq!( $node_id, payee) ;
762
+ false
763
+ } ;
764
+
765
+ if !skip_node && first_hops. is_some( ) {
714
766
if let Some ( & ( ref first_hop, ref features, ref outbound_capacity_msat) ) = first_hop_targets. get( & $node_id) {
715
767
add_entry!( first_hop, * our_node_id, $node_id, dummy_directional_info, Some ( outbound_capacity_msat / 1000 ) , features. to_context( ) , $fee_to_target_msat, $next_hops_value_contribution, $next_hops_path_htlc_minimum_msat) ;
716
768
}
@@ -723,7 +775,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
723
775
features = NodeFeatures :: empty( ) ;
724
776
}
725
777
726
- if !features. requires_unknown_bits( ) {
778
+ if !skip_node && ! features. requires_unknown_bits( ) {
727
779
for chan_id in $node. channels. iter( ) {
728
780
let chan = network. get_channels( ) . get( chan_id) . unwrap( ) ;
729
781
if !chan. features. requires_unknown_bits( ) {
@@ -942,6 +994,11 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
942
994
break ' path_construction;
943
995
}
944
996
997
+ // If we found a path back to the payee, we shouldn't try to process it again. This is
998
+ // the equivalent of the `elem.was_processed` check in
999
+ // add_entries_to_cheapest_to_target_node!() (see comment there for more info).
1000
+ if pubkey == * payee { continue ' path_construction; }
1001
+
945
1002
// Otherwise, since the current target node is not us,
946
1003
// keep "unrolling" the payment graph from payee to payer by
947
1004
// finding a way to reach the current target from the payer side.
0 commit comments