Add missing break when scoring a path with a missing channel 2022-10-removed-no-score-after
authorMatt Corallo <git@bluematt.me>
Mon, 31 Oct 2022 03:23:02 +0000 (03:23 +0000)
committerMatt Corallo <git@bluematt.me>
Wed, 2 Nov 2022 20:09:32 +0000 (20:09 +0000)
If we send payments over a path where a channel ended up being
closed, we'll remove it before we call
`ProbabilisticPaymentScorer::payment_path_failed`. This should be
fine, except that `payment_path_failed` does not break out of its
scoring loop if a channel is missing, causing it to assign a
minimum available-liquidity of the payment amount even to channels
which our attempt never arrived at.

The fix is simple - add the missing check and break.

lightning/src/routing/scoring.rs

index 5b829f61eb7536fc72f6adb350bd1ecaaba48646..860f3cb195418054322cd551f5e7487b84af8cad 100644 (file)
@@ -1143,31 +1143,32 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> Score for Probabilis
                                .get(&hop.short_channel_id)
                                .and_then(|channel| channel.as_directed_to(&target));
 
-                       if hop.short_channel_id == short_channel_id && hop_idx == 0 {
+                       let at_failed_channel = hop.short_channel_id == short_channel_id;
+                       if at_failed_channel && hop_idx == 0 {
                                log_warn!(self.logger, "Payment failed at the first hop - we do not attempt to learn channel info in such cases as we can directly observe local state.\n\tBecause we know the local state, we should generally not see failures here - this may be an indication that your channel peer on channel {} is broken and you may wish to close the channel.", hop.short_channel_id);
                        }
 
                        // Only score announced channels.
                        if let Some((channel, source)) = channel_directed_from_source {
                                let capacity_msat = channel.effective_capacity().as_msat();
-                               if hop.short_channel_id == short_channel_id {
+                               if at_failed_channel {
                                        self.channel_liquidities
                                                .entry(hop.short_channel_id)
                                                .or_insert_with(ChannelLiquidity::new)
                                                .as_directed_mut(source, &target, capacity_msat, &self.params)
                                                .failed_at_channel(amount_msat, format_args!("SCID {}, towards {:?}", hop.short_channel_id, target), &self.logger);
-                                       break;
+                               } else {
+                                       self.channel_liquidities
+                                               .entry(hop.short_channel_id)
+                                               .or_insert_with(ChannelLiquidity::new)
+                                               .as_directed_mut(source, &target, capacity_msat, &self.params)
+                                               .failed_downstream(amount_msat, format_args!("SCID {}, towards {:?}", hop.short_channel_id, target), &self.logger);
                                }
-
-                               self.channel_liquidities
-                                       .entry(hop.short_channel_id)
-                                       .or_insert_with(ChannelLiquidity::new)
-                                       .as_directed_mut(source, &target, capacity_msat, &self.params)
-                                       .failed_downstream(amount_msat, format_args!("SCID {}, towards {:?}", hop.short_channel_id, target), &self.logger);
                        } else {
                                log_debug!(self.logger, "Not able to penalize channel with SCID {} as we do not have graph info for it (likely a route-hint last-hop).",
                                        hop.short_channel_id);
                        }
+                       if at_failed_channel { break; }
                }
        }
 
@@ -1745,32 +1746,22 @@ mod tests {
                network_graph.update_channel(&signed_update).unwrap();
        }
 
+       fn path_hop(pubkey: PublicKey, short_channel_id: u64, fee_msat: u64) -> RouteHop {
+               RouteHop {
+                       pubkey,
+                       node_features: channelmanager::provided_node_features(),
+                       short_channel_id,
+                       channel_features: channelmanager::provided_channel_features(),
+                       fee_msat,
+                       cltv_expiry_delta: 18,
+               }
+       }
+
        fn payment_path_for_amount(amount_msat: u64) -> Vec<RouteHop> {
                vec![
-                       RouteHop {
-                               pubkey: source_pubkey(),
-                               node_features: channelmanager::provided_node_features(),
-                               short_channel_id: 41,
-                               channel_features: channelmanager::provided_channel_features(),
-                               fee_msat: 1,
-                               cltv_expiry_delta: 18,
-                       },
-                       RouteHop {
-                               pubkey: target_pubkey(),
-                               node_features: channelmanager::provided_node_features(),
-                               short_channel_id: 42,
-                               channel_features: channelmanager::provided_channel_features(),
-                               fee_msat: 2,
-                               cltv_expiry_delta: 18,
-                       },
-                       RouteHop {
-                               pubkey: recipient_pubkey(),
-                               node_features: channelmanager::provided_node_features(),
-                               short_channel_id: 43,
-                               channel_features: channelmanager::provided_channel_features(),
-                               fee_msat: amount_msat,
-                               cltv_expiry_delta: 18,
-                       },
+                       path_hop(source_pubkey(), 41, 1),
+                       path_hop(target_pubkey(), 42, 2),
+                       path_hop(recipient_pubkey(), 43, amount_msat),
                ]
        }
 
@@ -2145,6 +2136,65 @@ mod tests {
                assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
        }
 
+       #[test]
+       fn ignores_channels_after_removed_failed_channel() {
+               // Previously, if we'd tried to send over a channel which was removed from the network
+               // graph before we call `payment_path_failed` (which is the default if the we get a "no
+               // such channel" error in the `InvoicePayer`), we would call `failed_downstream` on all
+               // channels in the route, even ones which they payment never reached. This tests to ensure
+               // we do not score such channels.
+               let secp_ctx = Secp256k1::new();
+               let logger = TestLogger::new();
+               let genesis_hash = genesis_block(Network::Testnet).header.block_hash();
+               let mut network_graph = NetworkGraph::new(genesis_hash, &logger);
+               let secret_a = SecretKey::from_slice(&[42; 32]).unwrap();
+               let secret_b = SecretKey::from_slice(&[43; 32]).unwrap();
+               let secret_c = SecretKey::from_slice(&[44; 32]).unwrap();
+               let secret_d = SecretKey::from_slice(&[45; 32]).unwrap();
+               add_channel(&mut network_graph, 42, secret_a, secret_b);
+               // Don't add the channel from B -> C.
+               add_channel(&mut network_graph, 44, secret_c, secret_d);
+
+               let pub_a = PublicKey::from_secret_key(&secp_ctx, &secret_a);
+               let pub_b = PublicKey::from_secret_key(&secp_ctx, &secret_b);
+               let pub_c = PublicKey::from_secret_key(&secp_ctx, &secret_c);
+               let pub_d = PublicKey::from_secret_key(&secp_ctx, &secret_d);
+
+               let path = vec![
+                       path_hop(pub_b, 42, 1),
+                       path_hop(pub_c, 43, 2),
+                       path_hop(pub_d, 44, 100),
+               ];
+
+               let node_a = NodeId::from_pubkey(&pub_a);
+               let node_b = NodeId::from_pubkey(&pub_b);
+               let node_c = NodeId::from_pubkey(&pub_c);
+               let node_d = NodeId::from_pubkey(&pub_d);
+
+               let params = ProbabilisticScoringParameters {
+                       liquidity_penalty_multiplier_msat: 1_000,
+                       ..ProbabilisticScoringParameters::zero_penalty()
+               };
+               let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
+
+               let usage = ChannelUsage {
+                       amount_msat: 250,
+                       inflight_htlc_msat: 0,
+                       effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
+               };
+               assert_eq!(scorer.channel_penalty_msat(42, &node_a, &node_b, usage), 128);
+               // Note that a default liquidity bound is used for B -> C as no channel exists
+               assert_eq!(scorer.channel_penalty_msat(43, &node_b, &node_c, usage), 128);
+               assert_eq!(scorer.channel_penalty_msat(44, &node_c, &node_d, usage), 128);
+
+               scorer.payment_path_failed(&path.iter().collect::<Vec<_>>(), 43);
+
+               assert_eq!(scorer.channel_penalty_msat(42, &node_a, &node_b, usage), 80);
+               // Note that a default liquidity bound is used for B -> C as no channel exists
+               assert_eq!(scorer.channel_penalty_msat(43, &node_b, &node_c, usage), 128);
+               assert_eq!(scorer.channel_penalty_msat(44, &node_c, &node_d, usage), 128);
+       }
+
        #[test]
        fn reduces_liquidity_upper_bound_along_path_on_success() {
                let logger = TestLogger::new();