From 1f32ba46165b986ab63ee32a7104f812573c4cbe Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 21 Aug 2023 11:56:34 +0200 Subject: [PATCH 1/7] Remove mention of spontaneous payments from `lightning-invoice` --- lightning-invoice/src/payment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs index b67bac13f..e5cf41b1b 100644 --- a/lightning-invoice/src/payment.rs +++ b/lightning-invoice/src/payment.rs @@ -7,7 +7,7 @@ // You may not use this file except in accordance with one or both of these // licenses. -//! Convenient utilities for paying Lightning invoices and sending spontaneous payments. +//! Convenient utilities for paying Lightning invoices. use crate::Bolt11Invoice; From 758679af8401ce4a4f0a8964b6916be4a034b441 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 28 Aug 2023 13:57:29 +0200 Subject: [PATCH 2/7] Set `payment_secret` when sending probes Previously, we'd leave the payment secret field empty while sending probes, which resulted in having them rejected with `(PERM|invalid_onion_payload)` by Eclair nodes. In order to mitigate the issue, we just set a random payment secret. --- lightning/src/ln/outbound_payment.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 0cc9e7e05..127e73c20 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1103,6 +1103,7 @@ impl OutboundPayments { F: Fn(SendAlongPathArgs) -> Result<(), APIError>, { let payment_id = PaymentId(entropy_source.get_secure_random_bytes()); + let payment_secret = PaymentSecret(entropy_source.get_secure_random_bytes()); let payment_hash = probing_cookie_from_id(&payment_id, probing_cookie_secret); @@ -1114,7 +1115,7 @@ impl OutboundPayments { let route = Route { paths: vec![path], route_params: None }; let onion_session_privs = self.add_new_pending_payment(payment_hash, - RecipientOnionFields::spontaneous_empty(), payment_id, None, &route, None, None, + RecipientOnionFields::secret_only(payment_secret), payment_id, None, &route, None, None, entropy_source, best_block_height)?; match self.pay_route_internal(&route, payment_hash, RecipientOnionFields::spontaneous_empty(), From c6a1a12acaec7994694bb389aaa183719873ebee Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 12 Sep 2023 15:51:37 +0200 Subject: [PATCH 3/7] Include `maybe_announced` field in `RouteHop` When sending preflight probes, we want to exclude last hops that are possibly announced. To this end, we here include a new field in `RouteHop` that will be `true` when we either def. know the hop to be announced, or, if there exist public channels between the hop's counterparties that this hop might refer to (i.e., be an alias for). --- fuzz/src/chanmon_consistency.rs | 3 ++ lightning-background-processor/src/lib.rs | 1 + lightning/src/ln/functional_tests.rs | 14 +++++-- lightning/src/ln/onion_utils.rs | 10 ++--- lightning/src/ln/outbound_payment.rs | 2 + lightning/src/ln/payment_tests.rs | 20 +++++++++ lightning/src/routing/router.rs | 51 +++++++++++++++++++---- lightning/src/routing/scoring.rs | 1 + 8 files changed, 86 insertions(+), 16 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 4c79f0bee..b68310264 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -375,6 +375,7 @@ fn send_payment(source: &ChanMan, dest: &ChanMan, dest_chan_id: u64, amt: u64, p channel_features: dest.channel_features(), fee_msat: amt, cltv_expiry_delta: 200, + maybe_announced_channel: true, }], blinded_tail: None }], route_params: None, }, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_id)) { @@ -409,6 +410,7 @@ fn send_hop_payment(source: &ChanMan, middle: &ChanMan, middle_chan_id: u64, des channel_features: middle.channel_features(), fee_msat: first_hop_fee, cltv_expiry_delta: 100, + maybe_announced_channel: true, }, RouteHop { pubkey: dest.get_our_node_id(), node_features: dest.node_features(), @@ -416,6 +418,7 @@ fn send_hop_payment(source: &ChanMan, middle: &ChanMan, middle_chan_id: u64, des channel_features: dest.channel_features(), fee_msat: amt, cltv_expiry_delta: 200, + maybe_announced_channel: true, }], blinded_tail: None }], route_params: None, }, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_id)) { diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index 353ed6738..b3d1f3755 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -1685,6 +1685,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: 0, cltv_expiry_delta: MIN_CLTV_EXPIRY_DELTA as u32, + maybe_announced_channel: true, }], blinded_tail: None }; $nodes[0].scorer.lock().unwrap().expect(TestResult::PaymentFailure { path: path.clone(), short_channel_id: scored_scid }); diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index c0e334326..b252a352c 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1036,7 +1036,8 @@ fn fake_network_test() { short_channel_id: chan_2.0.contents.short_channel_id, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: chan_3.0.contents.cltv_expiry_delta as u32 + cltv_expiry_delta: chan_3.0.contents.cltv_expiry_delta as u32, + maybe_announced_channel: true, }); hops.push(RouteHop { pubkey: nodes[3].node.get_our_node_id(), @@ -1044,7 +1045,8 @@ fn fake_network_test() { short_channel_id: chan_3.0.contents.short_channel_id, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: chan_4.1.contents.cltv_expiry_delta as u32 + cltv_expiry_delta: chan_4.1.contents.cltv_expiry_delta as u32, + maybe_announced_channel: true, }); hops.push(RouteHop { pubkey: nodes[1].node.get_our_node_id(), @@ -1053,6 +1055,7 @@ fn fake_network_test() { channel_features: nodes[1].node.channel_features(), fee_msat: 1000000, cltv_expiry_delta: TEST_FINAL_CLTV, + maybe_announced_channel: true, }); hops[1].fee_msat = chan_4.1.contents.fee_base_msat as u64 + chan_4.1.contents.fee_proportional_millionths as u64 * hops[2].fee_msat as u64 / 1000000; hops[0].fee_msat = chan_3.0.contents.fee_base_msat as u64 + chan_3.0.contents.fee_proportional_millionths as u64 * hops[1].fee_msat as u64 / 1000000; @@ -1067,7 +1070,8 @@ fn fake_network_test() { short_channel_id: chan_4.0.contents.short_channel_id, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: chan_3.1.contents.cltv_expiry_delta as u32 + cltv_expiry_delta: chan_3.1.contents.cltv_expiry_delta as u32, + maybe_announced_channel: true, }); hops.push(RouteHop { pubkey: nodes[2].node.get_our_node_id(), @@ -1075,7 +1079,8 @@ fn fake_network_test() { short_channel_id: chan_3.0.contents.short_channel_id, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: chan_2.1.contents.cltv_expiry_delta as u32 + cltv_expiry_delta: chan_2.1.contents.cltv_expiry_delta as u32, + maybe_announced_channel: true, }); hops.push(RouteHop { pubkey: nodes[1].node.get_our_node_id(), @@ -1084,6 +1089,7 @@ fn fake_network_test() { channel_features: nodes[1].node.channel_features(), fee_msat: 1000000, cltv_expiry_delta: TEST_FINAL_CLTV, + maybe_announced_channel: true, }); hops[1].fee_msat = chan_2.1.contents.fee_base_msat as u64 + chan_2.1.contents.fee_proportional_millionths as u64 * hops[2].fee_msat as u64 / 1000000; hops[0].fee_msat = chan_3.1.contents.fee_base_msat as u64 + chan_3.1.contents.fee_proportional_millionths as u64 * hops[1].fee_msat as u64 / 1000000; diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 8fdbdefef..9b6babe34 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -984,27 +984,27 @@ mod tests { RouteHop { pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops. + short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops. }, RouteHop { pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops. + short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops. }, RouteHop { pubkey: PublicKey::from_slice(&hex::decode("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops. + short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops. }, RouteHop { pubkey: PublicKey::from_slice(&hex::decode("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops. + short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops. }, RouteHop { pubkey: PublicKey::from_slice(&hex::decode("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops. + short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops. }, ], blinded_tail: None }], route_params: None, diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 127e73c20..0d6586e40 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1853,6 +1853,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: 0, cltv_expiry_delta: 0, + maybe_announced_channel: true, }], blinded_tail: None }], route_params: Some(route_params.clone()), }; @@ -2153,6 +2154,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: invoice.amount_msats(), cltv_expiry_delta: 0, + maybe_announced_channel: true, } ], blinded_tail: None, diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index d88730c22..835ad67f1 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -2201,6 +2201,7 @@ fn auto_retry_partial_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: amt_msat / 2, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), @@ -2209,6 +2210,7 @@ fn auto_retry_partial_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: amt_msat / 2, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, ], route_params: Some(route_params.clone()), @@ -2222,6 +2224,7 @@ fn auto_retry_partial_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: amt_msat / 4, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), @@ -2230,6 +2233,7 @@ fn auto_retry_partial_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: amt_msat / 4, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, ], route_params: Some(route_params.clone()), @@ -2243,6 +2247,7 @@ fn auto_retry_partial_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: amt_msat / 4, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, ], route_params: Some(route_params.clone()), @@ -2487,6 +2492,7 @@ fn retry_multi_path_single_failed_payment() { channel_features: nodes[1].node.channel_features(), fee_msat: 10_000, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), @@ -2495,6 +2501,7 @@ fn retry_multi_path_single_failed_payment() { channel_features: nodes[1].node.channel_features(), fee_msat: 100_000_001, // Our default max-HTLC-value is 10% of the channel value, which this is one more than cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, ], route_params: Some(route_params.clone()), @@ -2576,6 +2583,7 @@ fn immediate_retry_on_failure() { channel_features: nodes[1].node.channel_features(), fee_msat: 100_000_001, // Our default max-HTLC-value is 10% of the channel value, which this is one more than cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, ], route_params: Some(RouteParameters::from_payment_params_and_value( @@ -2662,6 +2670,7 @@ fn no_extra_retries_on_back_to_back_fail() { channel_features: nodes[1].node.channel_features(), fee_msat: 0, // nodes[1] will fail the payment as we don't pay its fee cltv_expiry_delta: 100, + maybe_announced_channel: true, }, RouteHop { pubkey: nodes[2].node.get_our_node_id(), node_features: nodes[2].node.node_features(), @@ -2669,6 +2678,7 @@ fn no_extra_retries_on_back_to_back_fail() { channel_features: nodes[2].node.channel_features(), fee_msat: 100_000_000, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), @@ -2677,6 +2687,7 @@ fn no_extra_retries_on_back_to_back_fail() { channel_features: nodes[1].node.channel_features(), fee_msat: 0, // nodes[1] will fail the payment as we don't pay its fee cltv_expiry_delta: 100, + maybe_announced_channel: true, }, RouteHop { pubkey: nodes[2].node.get_our_node_id(), node_features: nodes[2].node.node_features(), @@ -2684,6 +2695,7 @@ fn no_extra_retries_on_back_to_back_fail() { channel_features: nodes[2].node.channel_features(), fee_msat: 100_000_000, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None } ], route_params: Some(RouteParameters::from_payment_params_and_value( @@ -2862,6 +2874,7 @@ fn test_simple_partial_retry() { channel_features: nodes[1].node.channel_features(), fee_msat: 0, // nodes[1] will fail the payment as we don't pay its fee cltv_expiry_delta: 100, + maybe_announced_channel: true, }, RouteHop { pubkey: nodes[2].node.get_our_node_id(), node_features: nodes[2].node.node_features(), @@ -2869,6 +2882,7 @@ fn test_simple_partial_retry() { channel_features: nodes[2].node.channel_features(), fee_msat: 100_000_000, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[1].node.get_our_node_id(), @@ -2877,6 +2891,7 @@ fn test_simple_partial_retry() { channel_features: nodes[1].node.channel_features(), fee_msat: 100_000, cltv_expiry_delta: 100, + maybe_announced_channel: true, }, RouteHop { pubkey: nodes[2].node.get_our_node_id(), node_features: nodes[2].node.node_features(), @@ -2884,6 +2899,7 @@ fn test_simple_partial_retry() { channel_features: nodes[2].node.channel_features(), fee_msat: 100_000_000, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None } ], route_params: Some(RouteParameters::from_payment_params_and_value( @@ -3026,6 +3042,7 @@ fn test_threaded_payment_retries() { channel_features: nodes[1].node.channel_features(), fee_msat: 0, cltv_expiry_delta: 100, + maybe_announced_channel: true, }, RouteHop { pubkey: nodes[3].node.get_our_node_id(), node_features: nodes[2].node.node_features(), @@ -3033,6 +3050,7 @@ fn test_threaded_payment_retries() { channel_features: nodes[2].node.channel_features(), fee_msat: amt_msat / 1000, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None }, Path { hops: vec![RouteHop { pubkey: nodes[2].node.get_our_node_id(), @@ -3041,6 +3059,7 @@ fn test_threaded_payment_retries() { channel_features: nodes[2].node.channel_features(), fee_msat: 100_000, cltv_expiry_delta: 100, + maybe_announced_channel: true, }, RouteHop { pubkey: nodes[3].node.get_our_node_id(), node_features: nodes[3].node.node_features(), @@ -3048,6 +3067,7 @@ fn test_threaded_payment_retries() { channel_features: nodes[3].node.channel_features(), fee_msat: amt_msat - amt_msat / 1000, cltv_expiry_delta: 100, + maybe_announced_channel: true, }], blinded_tail: None } ], route_params: Some(RouteParameters::from_payment_params_and_value( diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 9c5fe8e1f..1758cbbf9 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -250,10 +250,20 @@ pub struct RouteHop { /// /// [`BlindedPath`]: crate::blinded_path::BlindedPath pub cltv_expiry_delta: u32, + /// Indicates whether this hop is possibly announced in the public network graph. + /// + /// Will be `true` if there is a possibility that the channel is publicly known, i.e., if we + /// either know for sure it's announced in the public graph, or if any public channels exist + /// for which the given `short_channel_id` could be an alias for. Will be `false` if we believe + /// the channel to be unannounced. + /// + /// Will be `true` for objects serialized with LDK version 0.0.116 and before. + pub maybe_announced_channel: bool, } impl_writeable_tlv_based!(RouteHop, { (0, pubkey, required), + (1, maybe_announced_channel, (default_value, true)), (2, node_features, required), (4, short_channel_id, required), (6, channel_features, required), @@ -2472,9 +2482,27 @@ where L::Target: Logger { let mut paths = Vec::new(); for payment_path in selected_route { let mut hops = Vec::with_capacity(payment_path.hops.len()); + let mut prev_hop_node_id = our_node_id; for (hop, node_features) in payment_path.hops.iter() .filter(|(h, _)| h.candidate.short_channel_id().is_some()) { + let maybe_announced_channel = if let CandidateRouteHop::PublicHop { .. } = hop.candidate { + // If we sourced the hop from the graph we're sure the target node is announced. + true + } else if let CandidateRouteHop::FirstHop { details } = hop.candidate { + // If this is a first hop we also know if it's announced. + details.is_public + } else { + // If we sourced it any other way, we double-check the network graph to see if + // there are announced channels between the endpoints. If so, the hop might be + // referring to any of the announced channels, as its `short_channel_id` might be + // an alias, in which case we don't take any chances here. + network_graph.node(&hop.node_id).map_or(false, |hop_node| + hop_node.channels.iter().any(|scid| network_graph.channel(*scid) + .map_or(false, |c| c.as_directed_from(&prev_hop_node_id).is_some())) + ) + }; + hops.push(RouteHop { pubkey: PublicKey::from_slice(hop.node_id.as_slice()).map_err(|_| LightningError{err: format!("Public key {:?} is invalid", &hop.node_id), action: ErrorAction::IgnoreAndLog(Level::Trace)})?, node_features: node_features.clone(), @@ -2482,7 +2510,10 @@ where L::Target: Logger { channel_features: hop.candidate.features(), fee_msat: hop.fee_msat, cltv_expiry_delta: hop.candidate.cltv_expiry_delta(), + maybe_announced_channel, }); + + prev_hop_node_id = hop.node_id; } let mut final_cltv_delta = final_cltv_expiry_delta; let blinded_tail = payment_path.hops.last().and_then(|(h, _)| { @@ -5964,17 +5995,17 @@ mod tests { RouteHop { pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0 + short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0, maybe_announced_channel: true, }, RouteHop { pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0 + short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true, }, RouteHop { pubkey: PublicKey::from_slice(&hex::decode("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0 + short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0, maybe_announced_channel: true, }, ], blinded_tail: None }], route_params: None, @@ -5991,23 +6022,23 @@ mod tests { RouteHop { pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0 + short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0, maybe_announced_channel: true, }, RouteHop { pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0 + short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true, }, ], blinded_tail: None }, Path { hops: vec![ RouteHop { pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0 + short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0, maybe_announced_channel: true, }, RouteHop { pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(), channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(), - short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0 + short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true, }, ], blinded_tail: None }], route_params: None, @@ -6606,6 +6637,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: 100, cltv_expiry_delta: 0, + maybe_announced_channel: true, }], blinded_tail: Some(BlindedTail { hops: blinded_path_1.blinded_hops, @@ -6620,6 +6652,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: 100, cltv_expiry_delta: 0, + maybe_announced_channel: true, }], blinded_tail: None }], route_params: None, }; @@ -6659,6 +6692,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: 100, cltv_expiry_delta: 0, + maybe_announced_channel: false, }, RouteHop { pubkey: blinded_path.introduction_node_id, @@ -6667,6 +6701,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: 1, cltv_expiry_delta: 0, + maybe_announced_channel: false, }], blinded_tail: Some(BlindedTail { hops: blinded_path.blinded_hops, @@ -6699,6 +6734,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: 100, cltv_expiry_delta: 0, + maybe_announced_channel: false, }, RouteHop { pubkey: blinded_path.introduction_node_id, @@ -6707,6 +6743,7 @@ mod tests { channel_features: ChannelFeatures::empty(), fee_msat: 1, cltv_expiry_delta: 0, + maybe_announced_channel: false, } ], blinded_tail: Some(BlindedTail { diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs index aaafbc35b..c4f31db94 100644 --- a/lightning/src/routing/scoring.rs +++ b/lightning/src/routing/scoring.rs @@ -2014,6 +2014,7 @@ mod tests { channel_features: channelmanager::provided_channel_features(&config), fee_msat, cltv_expiry_delta: 18, + maybe_announced_channel: true, } } From 20c842b4969f6de53cba703cfa959153ac10cb80 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 28 Aug 2023 15:03:04 +0200 Subject: [PATCH 4/7] Add preflight probing capabilities We add a `ChannelManager::send_preflight_probes` method that can be used to send pre-flight probes given some [`RouteParameters`]. Additionally, we add convenience methods in for spontaneous probes and send pre-flight probes for a given invoice. As pre-flight probes might take up some of the available liquidity, we here introduce that channels whose available liquidity is less than the required amount times `UserConfig::preflight_probing_liquidity_limit_multiplier` won't be used to send pre-flight probes. This commit is a more or less a carbon copy of the pre-flight probing code recently added to LDK Node. --- lightning-invoice/src/payment.rs | 92 +++++++++++++++++++++++++++- lightning/src/ln/channelmanager.rs | 90 ++++++++++++++++++++++++++- lightning/src/ln/outbound_payment.rs | 14 ++++- 3 files changed, 192 insertions(+), 4 deletions(-) diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs index e5cf41b1b..6fa23d912 100644 --- a/lightning-invoice/src/payment.rs +++ b/lightning-invoice/src/payment.rs @@ -9,7 +9,7 @@ //! Convenient utilities for paying Lightning invoices. -use crate::Bolt11Invoice; +use crate::{Bolt11Invoice, Vec}; use bitcoin_hashes::Hash; @@ -17,7 +17,7 @@ use lightning::chain; use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; use lightning::sign::{NodeSigner, SignerProvider, EntropySource}; use lightning::ln::PaymentHash; -use lightning::ln::channelmanager::{ChannelManager, PaymentId, Retry, RetryableSendFailure, RecipientOnionFields}; +use lightning::ln::channelmanager::{ChannelManager, PaymentId, Retry, RetryableSendFailure, RecipientOnionFields, ProbeSendFailure}; use lightning::routing::router::{PaymentParameters, RouteParameters, Router}; use lightning::util::logger::Logger; @@ -163,6 +163,85 @@ fn pay_invoice_using_amount( payer.send_payment(payment_hash, recipient_onion, payment_id, route_params, retry_strategy) } +/// Sends payment probes over all paths of a route that would be used to pay the given invoice. +/// +/// See [`ChannelManager::send_preflight_probes`] for more information. +pub fn preflight_probe_invoice( + invoice: &Bolt11Invoice, channelmanager: &ChannelManager, + liquidity_limit_multiplier: Option, +) -> Result, ProbingError> +where + M::Target: chain::Watch<::Signer>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + L::Target: Logger, +{ + let amount_msat = if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { + invoice_amount_msat + } else { + return Err(ProbingError::Invoice("Failed to send probe as no amount was given in the invoice.")); + }; + + let mut payment_params = PaymentParameters::from_node_id( + invoice.recover_payee_pub_key(), + invoice.min_final_cltv_expiry_delta() as u32, + ) + .with_expiry_time(expiry_time_from_unix_epoch(invoice).as_secs()) + .with_route_hints(invoice.route_hints()) + .unwrap(); + + if let Some(features) = invoice.features() { + payment_params = payment_params.with_bolt11_features(features.clone()).unwrap(); + } + let route_params = RouteParameters { payment_params, final_value_msat: amount_msat }; + + channelmanager.send_preflight_probes(route_params, liquidity_limit_multiplier) + .map_err(ProbingError::Sending) +} + +/// Sends payment probes over all paths of a route that would be used to pay the given zero-value +/// invoice using the given amount. +/// +/// See [`ChannelManager::send_preflight_probes`] for more information. +pub fn preflight_probe_zero_value_invoice( + invoice: &Bolt11Invoice, amount_msat: u64, channelmanager: &ChannelManager, + liquidity_limit_multiplier: Option, +) -> Result, ProbingError> +where + M::Target: chain::Watch<::Signer>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + L::Target: Logger, +{ + if invoice.amount_milli_satoshis().is_some() { + return Err(ProbingError::Invoice("amount unexpected")); + } + + let mut payment_params = PaymentParameters::from_node_id( + invoice.recover_payee_pub_key(), + invoice.min_final_cltv_expiry_delta() as u32, + ) + .with_expiry_time(expiry_time_from_unix_epoch(invoice).as_secs()) + .with_route_hints(invoice.route_hints()) + .unwrap(); + + if let Some(features) = invoice.features() { + payment_params = payment_params.with_bolt11_features(features.clone()).unwrap(); + } + let route_params = RouteParameters { payment_params, final_value_msat: amount_msat }; + + channelmanager.send_preflight_probes(route_params, liquidity_limit_multiplier) + .map_err(ProbingError::Sending) +} + fn expiry_time_from_unix_epoch(invoice: &Bolt11Invoice) -> Duration { invoice.signed_invoice.raw_invoice.data.timestamp.0 + invoice.expiry_time() } @@ -176,6 +255,15 @@ pub enum PaymentError { Sending(RetryableSendFailure), } +/// An error that may occur when sending a payment probe. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ProbingError { + /// An error resulting from the provided [`Bolt11Invoice`]. + Invoice(&'static str), + /// An error occurring when sending a payment probe. + Sending(ProbeSendFailure), +} + /// A trait defining behavior of a [`Bolt11Invoice`] payer. /// /// Useful for unit testing internal methods. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e2d7c90f7..1b971dce4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -77,7 +77,7 @@ use core::time::Duration; use core::ops::Deref; // Re-export this for use in the public API. -pub use crate::ln::outbound_payment::{PaymentSendFailure, Retry, RetryableSendFailure, RecipientOnionFields}; +pub use crate::ln::outbound_payment::{PaymentSendFailure, ProbeSendFailure, Retry, RetryableSendFailure, RecipientOnionFields}; use crate::ln::script::ShutdownScript; // We hold various information about HTLC relay in the HTLC objects in Channel itself: @@ -3488,6 +3488,94 @@ where outbound_payment::payment_is_probe(payment_hash, payment_id, self.probing_cookie_secret) } + /// Sends payment probes over all paths of a route that would be used to pay the given + /// amount to the given `node_id`. + /// + /// See [`ChannelManager::send_preflight_probes`] for more information. + pub fn send_spontaneous_preflight_probes( + &self, node_id: PublicKey, amount_msat: u64, final_cltv_expiry_delta: u32, + liquidity_limit_multiplier: Option, + ) -> Result, ProbeSendFailure> { + let payment_params = + PaymentParameters::from_node_id(node_id, final_cltv_expiry_delta); + + let route_params = RouteParameters { payment_params, final_value_msat: amount_msat }; + + self.send_preflight_probes(route_params, liquidity_limit_multiplier) + } + + /// Sends payment probes over all paths of a route that would be used to pay a route found + /// according to the given [`RouteParameters`]. + /// + /// This may be used to send "pre-flight" probes, i.e., to train our scorer before conducting + /// the actual payment. Note this is only useful if there likely is sufficient time for the + /// probe to settle before sending out the actual payment, e.g., when waiting for user + /// confirmation in a wallet UI. + /// + /// Otherwise, there is a chance the probe could take up some liquidity needed to complete the + /// actual payment. Users should therefore be cautious and might avoid sending probes if + /// liquidity is scarce and/or they don't expect the probe to return before they send the + /// payment. To mitigate this issue, channels with available liquidity less than the required + /// amount times the given `liquidity_limit_multiplier` won't be used to send pre-flight + /// probes. If `None` is given as `liquidity_limit_multiplier`, it defaults to `3`. + pub fn send_preflight_probes( + &self, route_params: RouteParameters, liquidity_limit_multiplier: Option, + ) -> Result, ProbeSendFailure> { + let liquidity_limit_multiplier = liquidity_limit_multiplier.unwrap_or(3); + + let payer = self.get_our_node_id(); + let usable_channels = self.list_usable_channels(); + let first_hops = usable_channels.iter().collect::>(); + let inflight_htlcs = self.compute_inflight_htlcs(); + + let route = self + .router + .find_route(&payer, &route_params, Some(&first_hops), inflight_htlcs) + .map_err(|e| { + log_error!(self.logger, "Failed to find path for payment probe: {:?}", e); + ProbeSendFailure::RouteNotFound + })?; + + let mut used_liquidity_map = HashMap::with_capacity(first_hops.len()); + + let mut res = Vec::new(); + for path in route.paths { + if path.hops.len() < 2 { + log_debug!( + self.logger, + "Skipped sending payment probe over path with less than two hops." + ); + continue; + } + + if let Some(first_path_hop) = path.hops.first() { + if let Some(first_hop) = first_hops.iter().find(|h| { + h.get_outbound_payment_scid() == Some(first_path_hop.short_channel_id) + }) { + let path_value = path.final_value_msat() + path.fee_msat(); + let used_liquidity = + used_liquidity_map.entry(first_path_hop.short_channel_id).or_insert(0); + + if first_hop.next_outbound_htlc_limit_msat + < (*used_liquidity + path_value) * liquidity_limit_multiplier + { + log_debug!(self.logger, "Skipped sending payment probe to avoid putting channel {} under the liquidity limit.", first_path_hop.short_channel_id); + continue; + } else { + *used_liquidity += path_value; + } + } + } + + res.push(self.send_probe(path).map_err(|e| { + log_error!(self.logger, "Failed to send pre-flight probe: {:?}", e); + ProbeSendFailure::SendingFailed(e) + })?); + } + + Ok(res) + } + /// Handles the generation of a funding transaction, optionally (for tests) with a function /// which checks the correctness of the funding transaction given the associated channel. fn funding_transaction_generated_intern, &Transaction) -> Result>( diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 0d6586e40..583c54056 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -391,7 +391,7 @@ pub enum RetryableSendFailure { /// is in, see the description of individual enum states for more. /// /// [`ChannelManager::send_payment_with_route`]: crate::ln::channelmanager::ChannelManager::send_payment_with_route -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum PaymentSendFailure { /// A parameter which was passed to send_payment was invalid, preventing us from attempting to /// send the payment at all. @@ -465,6 +465,18 @@ pub(super) enum Bolt12PaymentError { DuplicateInvoice, } +/// Indicates that we failed to send a payment probe. Further errors may be surfaced later via +/// [`Event::ProbeFailed`]. +/// +/// [`Event::ProbeFailed`]: crate::events::Event::ProbeFailed +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ProbeSendFailure { + /// We were unable to find a route to the destination. + RouteNotFound, + /// We failed to send the payment probes. + SendingFailed(PaymentSendFailure), +} + /// Information which is provided, encrypted, to the payment recipient when sending HTLCs. /// /// This should generally be constructed with data communicated to us from the recipient (via a From cdb87722027fed31ef2207ef7376e64196290201 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 1 Sep 2023 16:39:04 +0200 Subject: [PATCH 5/7] Test preflight probing sends and skips if necessary --- lightning/src/ln/payment_tests.rs | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 835ad67f1..3def4e362 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -25,6 +25,7 @@ use crate::ln::outbound_payment::{IDEMPOTENCY_TIMEOUT_TICKS, Retry}; use crate::routing::gossip::{EffectiveCapacity, RoutingFees}; use crate::routing::router::{get_route, Path, PaymentParameters, Route, Router, RouteHint, RouteHintHop, RouteHop, RouteParameters, find_route}; use crate::routing::scoring::ChannelUsage; +use crate::util::config::UserConfig; use crate::util::test_utils; use crate::util::errors::APIError; use crate::util::ser::Writeable; @@ -1304,6 +1305,102 @@ fn onchain_failed_probe_yields_event() { assert!(!nodes[0].node.has_pending_payments()); } +#[test] +fn preflight_probes_yield_event_and_skip() { + let chanmon_cfgs = create_chanmon_cfgs(5); + let node_cfgs = create_node_cfgs(5, &chanmon_cfgs); + + // We alleviate the HTLC max-in-flight limit, as otherwise we'd always be limited through that. + let mut no_htlc_limit_config = test_default_channel_config(); + no_htlc_limit_config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + + let user_configs = std::iter::repeat(no_htlc_limit_config).take(5).map(|c| Some(c)).collect::>>(); + let node_chanmgrs = create_node_chanmgrs(5, &node_cfgs, &user_configs); + let nodes = create_network(5, &node_cfgs, &node_chanmgrs); + + // Setup channel topology: + // (30k:0)- N2 -(1M:0) + // / \ + // N0 -(100k:0)-> N1 N4 + // \ / + // (70k:0)- N3 -(1M:0) + // + let first_chan_update = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0).0; + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 30_000, 0); + create_announced_chan_between_nodes_with_value(&nodes, 1, 3, 70_000, 0); + create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 1_000_000, 0); + create_announced_chan_between_nodes_with_value(&nodes, 3, 4, 1_000_000, 0); + + let mut invoice_features = Bolt11InvoiceFeatures::empty(); + invoice_features.set_basic_mpp_optional(); + + let mut payment_params = PaymentParameters::from_node_id(nodes[4].node.get_our_node_id(), TEST_FINAL_CLTV) + .with_bolt11_features(invoice_features).unwrap(); + + let route_params = RouteParameters { payment_params, final_value_msat: 80_000_000 }; + let res = nodes[0].node.send_preflight_probes(route_params, None).unwrap(); + + // We check that only one probe was sent, the other one was skipped due to limited liquidity. + assert_eq!(res.len(), 1); + let log_msg = format!("Skipped sending payment probe to avoid putting channel {} under the liquidity limit.", + first_chan_update.contents.short_channel_id); + node_cfgs[0].logger.assert_log_contains("lightning::ln::channelmanager", &log_msg, 1); + + let (payment_hash, payment_id) = res.first().unwrap(); + + // node[0] -- update_add_htlcs -> node[1] + check_added_monitors!(nodes[0], 1); + let probe_event = SendEvent::from_node(&nodes[0]); + nodes[1].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &probe_event.msgs[0]); + check_added_monitors!(nodes[1], 0); + commitment_signed_dance!(nodes[1], nodes[0], probe_event.commitment_msg, false); + expect_pending_htlcs_forwardable!(nodes[1]); + + // node[1] -- update_add_htlcs -> node[2] + check_added_monitors!(nodes[1], 1); + let probe_event = SendEvent::from_node(&nodes[1]); + nodes[2].node.handle_update_add_htlc(&nodes[1].node.get_our_node_id(), &probe_event.msgs[0]); + check_added_monitors!(nodes[2], 0); + commitment_signed_dance!(nodes[2], nodes[1], probe_event.commitment_msg, false); + expect_pending_htlcs_forwardable!(nodes[2]); + + // node[2] -- update_add_htlcs -> node[4] + check_added_monitors!(nodes[2], 1); + let probe_event = SendEvent::from_node(&nodes[2]); + nodes[4].node.handle_update_add_htlc(&nodes[2].node.get_our_node_id(), &probe_event.msgs[0]); + check_added_monitors!(nodes[4], 0); + commitment_signed_dance!(nodes[4], nodes[2], probe_event.commitment_msg, true, true); + + // node[2] <- update_fail_htlcs -- node[4] + let updates = get_htlc_update_msgs!(nodes[4], nodes[2].node.get_our_node_id()); + nodes[2].node.handle_update_fail_htlc(&nodes[4].node.get_our_node_id(), &updates.update_fail_htlcs[0]); + check_added_monitors!(nodes[2], 0); + commitment_signed_dance!(nodes[2], nodes[4], updates.commitment_signed, true); + + // node[1] <- update_fail_htlcs -- node[2] + let updates = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id()); + nodes[1].node.handle_update_fail_htlc(&nodes[2].node.get_our_node_id(), &updates.update_fail_htlcs[0]); + check_added_monitors!(nodes[1], 0); + commitment_signed_dance!(nodes[1], nodes[2], updates.commitment_signed, true); + + // node[0] <- update_fail_htlcs -- node[1] + let updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); + nodes[0].node.handle_update_fail_htlc(&nodes[1].node.get_our_node_id(), &updates.update_fail_htlcs[0]); + check_added_monitors!(nodes[0], 0); + commitment_signed_dance!(nodes[0], nodes[1], updates.commitment_signed, false); + + let mut events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events.drain(..).next().unwrap() { + crate::events::Event::ProbeSuccessful { payment_id: ev_pid, payment_hash: ev_ph, .. } => { + assert_eq!(*payment_id, ev_pid); + assert_eq!(*payment_hash, ev_ph); + }, + _ => panic!(), + }; + assert!(!nodes[0].node.has_pending_payments()); +} + #[test] fn claimed_send_payment_idempotent() { // Tests that `send_payment` (and friends) are (reasonably) idempotent. From 30e47ca56cd7ab2ed46a79bd5b0567e79a7d7941 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 11 Sep 2023 13:50:10 +0200 Subject: [PATCH 6/7] Probe up to second-to-last hop if last was provided by route hint If the last hop was provided by route hint we assume it's not an announced channel. If furthermore only a single route hint is provided we refrain from probing through all the way to the end and instead probe up to the second-to-last channel. Optimally we'd do this not based on above mentioned assumption but rather by checking inclusion in our network graph. However, we don't have access to our graph in `ChannelManager`. --- lightning/src/ln/channelmanager.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 1b971dce4..2748062c9 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3539,7 +3539,29 @@ where let mut used_liquidity_map = HashMap::with_capacity(first_hops.len()); let mut res = Vec::new(); - for path in route.paths { + + for mut path in route.paths { + // If the last hop is probably an unannounced channel we refrain from probing all the + // way through to the end and instead probe up to the second-to-last channel. + while let Some(last_path_hop) = path.hops.last() { + if last_path_hop.maybe_announced_channel { + // We found a potentially announced last hop. + break; + } else { + // Drop the last hop, as it's likely unannounced. + log_debug!( + self.logger, + "Avoided sending payment probe all the way to last hop {} as it is likely unannounced.", + last_path_hop.short_channel_id + ); + let final_value_msat = path.final_value_msat(); + path.hops.pop(); + if let Some(new_last) = path.hops.last_mut() { + new_last.fee_msat += final_value_msat; + } + } + } + if path.hops.len() < 2 { log_debug!( self.logger, From f75ac9addfccd20bb15a6c129fe2b558f50fc453 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 12 Sep 2023 13:20:31 +0200 Subject: [PATCH 7/7] Expose `AChannelManager` trait and use it in `lightning-invoice` --- lightning-invoice/src/payment.rs | 94 ++++++------------------------ lightning/src/ln/channelmanager.rs | 29 ++++++--- 2 files changed, 39 insertions(+), 84 deletions(-) diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs index 6fa23d912..f5b20d87f 100644 --- a/lightning-invoice/src/payment.rs +++ b/lightning-invoice/src/payment.rs @@ -17,7 +17,7 @@ use lightning::chain; use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; use lightning::sign::{NodeSigner, SignerProvider, EntropySource}; use lightning::ln::PaymentHash; -use lightning::ln::channelmanager::{ChannelManager, PaymentId, Retry, RetryableSendFailure, RecipientOnionFields, ProbeSendFailure}; +use lightning::ln::channelmanager::{AChannelManager, ChannelManager, PaymentId, Retry, RetryableSendFailure, RecipientOnionFields, ProbeSendFailure}; use lightning::routing::router::{PaymentParameters, RouteParameters, Router}; use lightning::util::logger::Logger; @@ -32,22 +32,12 @@ use core::time::Duration; /// with the same [`PaymentHash`] is never sent. /// /// If you wish to use a different payment idempotency token, see [`pay_invoice_with_id`]. -pub fn pay_invoice( - invoice: &Bolt11Invoice, retry_strategy: Retry, - channelmanager: &ChannelManager +pub fn pay_invoice( + invoice: &Bolt11Invoice, retry_strategy: Retry, channelmanager: &C ) -> Result -where - M::Target: chain::Watch<::Signer>, - T::Target: BroadcasterInterface, - ES::Target: EntropySource, - NS::Target: NodeSigner, - SP::Target: SignerProvider, - F::Target: FeeEstimator, - R::Target: Router, - L::Target: Logger, { let payment_id = PaymentId(invoice.payment_hash().into_inner()); - pay_invoice_with_id(invoice, payment_id, retry_strategy, channelmanager) + pay_invoice_with_id(invoice, payment_id, retry_strategy, channelmanager.get_cm()) .map(|()| payment_id) } @@ -61,22 +51,12 @@ where /// [`PaymentHash`] has never been paid before. /// /// See [`pay_invoice`] for a variant which uses the [`PaymentHash`] for the idempotency token. -pub fn pay_invoice_with_id( - invoice: &Bolt11Invoice, payment_id: PaymentId, retry_strategy: Retry, - channelmanager: &ChannelManager +pub fn pay_invoice_with_id( + invoice: &Bolt11Invoice, payment_id: PaymentId, retry_strategy: Retry, channelmanager: &C ) -> Result<(), PaymentError> -where - M::Target: chain::Watch<::Signer>, - T::Target: BroadcasterInterface, - ES::Target: EntropySource, - NS::Target: NodeSigner, - SP::Target: SignerProvider, - F::Target: FeeEstimator, - R::Target: Router, - L::Target: Logger, { let amt_msat = invoice.amount_milli_satoshis().ok_or(PaymentError::Invoice("amount missing"))?; - pay_invoice_using_amount(invoice, amt_msat, payment_id, retry_strategy, channelmanager) + pay_invoice_using_amount(invoice, amt_msat, payment_id, retry_strategy, channelmanager.get_cm()) } /// Pays the given zero-value [`Bolt11Invoice`] using the given amount, retrying if needed based on @@ -88,19 +68,9 @@ where /// /// If you wish to use a different payment idempotency token, see /// [`pay_zero_value_invoice_with_id`]. -pub fn pay_zero_value_invoice( - invoice: &Bolt11Invoice, amount_msats: u64, retry_strategy: Retry, - channelmanager: &ChannelManager +pub fn pay_zero_value_invoice( + invoice: &Bolt11Invoice, amount_msats: u64, retry_strategy: Retry, channelmanager: &C ) -> Result -where - M::Target: chain::Watch<::Signer>, - T::Target: BroadcasterInterface, - ES::Target: EntropySource, - NS::Target: NodeSigner, - SP::Target: SignerProvider, - F::Target: FeeEstimator, - R::Target: Router, - L::Target: Logger, { let payment_id = PaymentId(invoice.payment_hash().into_inner()); pay_zero_value_invoice_with_id(invoice, amount_msats, payment_id, retry_strategy, @@ -119,25 +89,16 @@ where /// /// See [`pay_zero_value_invoice`] for a variant which uses the [`PaymentHash`] for the /// idempotency token. -pub fn pay_zero_value_invoice_with_id( +pub fn pay_zero_value_invoice_with_id( invoice: &Bolt11Invoice, amount_msats: u64, payment_id: PaymentId, retry_strategy: Retry, - channelmanager: &ChannelManager + channelmanager: &C ) -> Result<(), PaymentError> -where - M::Target: chain::Watch<::Signer>, - T::Target: BroadcasterInterface, - ES::Target: EntropySource, - NS::Target: NodeSigner, - SP::Target: SignerProvider, - F::Target: FeeEstimator, - R::Target: Router, - L::Target: Logger, { if invoice.amount_milli_satoshis().is_some() { Err(PaymentError::Invoice("amount unexpected")) } else { pay_invoice_using_amount(invoice, amount_msats, payment_id, retry_strategy, - channelmanager) + channelmanager.get_cm()) } } @@ -166,19 +127,9 @@ fn pay_invoice_using_amount( /// Sends payment probes over all paths of a route that would be used to pay the given invoice. /// /// See [`ChannelManager::send_preflight_probes`] for more information. -pub fn preflight_probe_invoice( - invoice: &Bolt11Invoice, channelmanager: &ChannelManager, - liquidity_limit_multiplier: Option, +pub fn preflight_probe_invoice( + invoice: &Bolt11Invoice, channelmanager: &C, liquidity_limit_multiplier: Option, ) -> Result, ProbingError> -where - M::Target: chain::Watch<::Signer>, - T::Target: BroadcasterInterface, - ES::Target: EntropySource, - NS::Target: NodeSigner, - SP::Target: SignerProvider, - F::Target: FeeEstimator, - R::Target: Router, - L::Target: Logger, { let amount_msat = if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { invoice_amount_msat @@ -199,7 +150,7 @@ where } let route_params = RouteParameters { payment_params, final_value_msat: amount_msat }; - channelmanager.send_preflight_probes(route_params, liquidity_limit_multiplier) + channelmanager.get_cm().send_preflight_probes(route_params, liquidity_limit_multiplier) .map_err(ProbingError::Sending) } @@ -207,19 +158,10 @@ where /// invoice using the given amount. /// /// See [`ChannelManager::send_preflight_probes`] for more information. -pub fn preflight_probe_zero_value_invoice( - invoice: &Bolt11Invoice, amount_msat: u64, channelmanager: &ChannelManager, +pub fn preflight_probe_zero_value_invoice( + invoice: &Bolt11Invoice, amount_msat: u64, channelmanager: &C, liquidity_limit_multiplier: Option, ) -> Result, ProbingError> -where - M::Target: chain::Watch<::Signer>, - T::Target: BroadcasterInterface, - ES::Target: EntropySource, - NS::Target: NodeSigner, - SP::Target: SignerProvider, - F::Target: FeeEstimator, - R::Target: Router, - L::Target: Logger, { if invoice.amount_milli_satoshis().is_some() { return Err(ProbingError::Invoice("amount unexpected")); @@ -238,7 +180,7 @@ where } let route_params = RouteParameters { payment_params, final_value_msat: amount_msat }; - channelmanager.send_preflight_probes(route_params, liquidity_limit_multiplier) + channelmanager.get_cm().send_preflight_probes(route_params, liquidity_limit_multiplier) .map_err(ProbingError::Sending) } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 2748062c9..785595a89 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -835,33 +835,46 @@ pub type SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, M, T, F, L> = &'g L >; -macro_rules! define_test_pub_trait { ($vis: vis) => { -/// A trivial trait which describes any [`ChannelManager`] used in testing. -$vis trait AChannelManager { +/// A trivial trait which describes any [`ChannelManager`]. +pub trait AChannelManager { + /// A type implementing [`chain::Watch`]. type Watch: chain::Watch + ?Sized; + /// A type that may be dereferenced to [`Self::Watch`]. type M: Deref; + /// A type implementing [`BroadcasterInterface`]. type Broadcaster: BroadcasterInterface + ?Sized; + /// A type that may be dereferenced to [`Self::Broadcaster`]. type T: Deref; + /// A type implementing [`EntropySource`]. type EntropySource: EntropySource + ?Sized; + /// A type that may be dereferenced to [`Self::EntropySource`]. type ES: Deref; + /// A type implementing [`NodeSigner`]. type NodeSigner: NodeSigner + ?Sized; + /// A type that may be dereferenced to [`Self::NodeSigner`]. type NS: Deref; + /// A type implementing [`WriteableEcdsaChannelSigner`]. type Signer: WriteableEcdsaChannelSigner + Sized; + /// A type implementing [`SignerProvider`] for [`Self::Signer`]. type SignerProvider: SignerProvider + ?Sized; + /// A type that may be dereferenced to [`Self::SignerProvider`]. type SP: Deref; + /// A type implementing [`FeeEstimator`]. type FeeEstimator: FeeEstimator + ?Sized; + /// A type that may be dereferenced to [`Self::FeeEstimator`]. type F: Deref; + /// A type implementing [`Router`]. type Router: Router + ?Sized; + /// A type that may be dereferenced to [`Self::Router`]. type R: Deref; + /// A type implementing [`Logger`]. type Logger: Logger + ?Sized; + /// A type that may be dereferenced to [`Self::Logger`]. type L: Deref; + /// Returns a reference to the actual [`ChannelManager`] object. fn get_cm(&self) -> &ChannelManager; } -} } -#[cfg(any(test, feature = "_test_utils"))] -define_test_pub_trait!(pub); -#[cfg(not(any(test, feature = "_test_utils")))] -define_test_pub_trait!(pub(crate)); + impl AChannelManager for ChannelManager where