Merge pull request #2534 from tnull/2023-08-upstream-preflight-probing
authorMatt Corallo <649246+TheBlueMatt@users.noreply.github.com>
Mon, 18 Sep 2023 16:41:57 +0000 (16:41 +0000)
committerGitHub <noreply@github.com>
Mon, 18 Sep 2023 16:41:57 +0000 (16:41 +0000)
Upstream and fix preflight probing

fuzz/src/chanmon_consistency.rs
lightning-background-processor/src/lib.rs
lightning-invoice/src/payment.rs
lightning/src/ln/channelmanager.rs
lightning/src/ln/functional_tests.rs
lightning/src/ln/onion_utils.rs
lightning/src/ln/outbound_payment.rs
lightning/src/ln/payment_tests.rs
lightning/src/routing/router.rs
lightning/src/routing/scoring.rs

index 8afc2e15187edb5397474a64b2cf33e34359a667..b6d41fb99140cd2cb012ffbaf08c190efdd443fd 100644 (file)
@@ -371,6 +371,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)) {
@@ -405,6 +406,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(),
@@ -412,6 +414,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)) {
index 6a36874a384bf9ca23a91d2cef6557853943aa2f..7ae14b4b4aabd5e92c3a610b5cfd622c2314264f 100644 (file)
@@ -1683,6 +1683,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 });
index b67bac13f3447f2061782266a52812fee0cfcac5..f5b20d87fa192f25f2ee709749d9cd903d71792d 100644 (file)
@@ -7,9 +7,9 @@
 // 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;
+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::{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<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
-       invoice: &Bolt11Invoice, retry_strategy: Retry,
-       channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>
+pub fn pay_invoice<C: AChannelManager>(
+       invoice: &Bolt11Invoice, retry_strategy: Retry, channelmanager: &C
 ) -> Result<PaymentId, PaymentError>
-where
-               M::Target: chain::Watch<<SP::Target as SignerProvider>::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<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
-       invoice: &Bolt11Invoice, payment_id: PaymentId, retry_strategy: Retry,
-       channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>
+pub fn pay_invoice_with_id<C: AChannelManager>(
+       invoice: &Bolt11Invoice, payment_id: PaymentId, retry_strategy: Retry, channelmanager: &C
 ) -> Result<(), PaymentError>
-where
-               M::Target: chain::Watch<<SP::Target as SignerProvider>::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<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
-       invoice: &Bolt11Invoice, amount_msats: u64, retry_strategy: Retry,
-       channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>
+pub fn pay_zero_value_invoice<C: AChannelManager>(
+       invoice: &Bolt11Invoice, amount_msats: u64, retry_strategy: Retry, channelmanager: &C
 ) -> Result<PaymentId, PaymentError>
-where
-               M::Target: chain::Watch<<SP::Target as SignerProvider>::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<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
+pub fn pay_zero_value_invoice_with_id<C: AChannelManager>(
        invoice: &Bolt11Invoice, amount_msats: u64, payment_id: PaymentId, retry_strategy: Retry,
-       channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>
+       channelmanager: &C
 ) -> Result<(), PaymentError>
-where
-               M::Target: chain::Watch<<SP::Target as SignerProvider>::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())
        }
 }
 
@@ -163,6 +124,66 @@ fn pay_invoice_using_amount<P: Deref>(
        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<C: AChannelManager>(
+       invoice: &Bolt11Invoice, channelmanager: &C, liquidity_limit_multiplier: Option<u64>,
+) -> Result<Vec<(PaymentHash, PaymentId)>, ProbingError>
+{
+       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.get_cm().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<C: AChannelManager>(
+       invoice: &Bolt11Invoice, amount_msat: u64, channelmanager: &C,
+       liquidity_limit_multiplier: Option<u64>,
+) -> Result<Vec<(PaymentHash, PaymentId)>, ProbingError>
+{
+       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.get_cm().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 +197,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.
index 90451d59d66d635821ffbe3ce32790d471290c6c..0cadbd41a29d21ccb99de1e0d7bf84319e33971c 100644 (file)
@@ -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:
@@ -839,33 +839,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<Self::Signer> + ?Sized;
+       /// A type that may be dereferenced to [`Self::Watch`].
        type M: Deref<Target = Self::Watch>;
+       /// A type implementing [`BroadcasterInterface`].
        type Broadcaster: BroadcasterInterface + ?Sized;
+       /// A type that may be dereferenced to [`Self::Broadcaster`].
        type T: Deref<Target = Self::Broadcaster>;
+       /// A type implementing [`EntropySource`].
        type EntropySource: EntropySource + ?Sized;
+       /// A type that may be dereferenced to [`Self::EntropySource`].
        type ES: Deref<Target = Self::EntropySource>;
+       /// A type implementing [`NodeSigner`].
        type NodeSigner: NodeSigner + ?Sized;
+       /// A type that may be dereferenced to [`Self::NodeSigner`].
        type NS: Deref<Target = Self::NodeSigner>;
+       /// A type implementing [`WriteableEcdsaChannelSigner`].
        type Signer: WriteableEcdsaChannelSigner + Sized;
+       /// A type implementing [`SignerProvider`] for [`Self::Signer`].
        type SignerProvider: SignerProvider<Signer = Self::Signer> + ?Sized;
+       /// A type that may be dereferenced to [`Self::SignerProvider`].
        type SP: Deref<Target = Self::SignerProvider>;
+       /// A type implementing [`FeeEstimator`].
        type FeeEstimator: FeeEstimator + ?Sized;
+       /// A type that may be dereferenced to [`Self::FeeEstimator`].
        type F: Deref<Target = Self::FeeEstimator>;
+       /// A type implementing [`Router`].
        type Router: Router + ?Sized;
+       /// A type that may be dereferenced to [`Self::Router`].
        type R: Deref<Target = Self::Router>;
+       /// A type implementing [`Logger`].
        type Logger: Logger + ?Sized;
+       /// A type that may be dereferenced to [`Self::Logger`].
        type L: Deref<Target = Self::Logger>;
+       /// Returns a reference to the actual [`ChannelManager`] object.
        fn get_cm(&self) -> &ChannelManager<Self::M, Self::T, Self::ES, Self::NS, Self::SP, Self::F, Self::R, Self::L>;
 }
-} }
-#[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<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref> AChannelManager
 for ChannelManager<M, T, ES, NS, SP, F, R, L>
 where
@@ -3546,6 +3559,116 @@ 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<u64>,
+       ) -> Result<Vec<(PaymentHash, PaymentId)>, 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<u64>,
+       ) -> Result<Vec<(PaymentHash, PaymentId)>, 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::<Vec<_>>();
+               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 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,
+                                       "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<FundingOutput: Fn(&OutboundV1Channel<SP>, &Transaction) -> Result<OutPoint, APIError>>(
index c0e33432611bd4aacc29f15b3affdea8fb1c7e98..b252a352c48df981c2a2d102ba188c9943942143 100644 (file)
@@ -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;
index 666221b2dd47fbe400b506d7602435905f9a2010..52cd3ca96d94760f71fcc1d354dc60e79360f427 100644 (file)
@@ -1014,27 +1014,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,
index 5ea772e5d4ffbfb0b82c9fc19e014781e3219aa0..023412e1afb56cdcdd0a2d9195064e5744ccab6c 100644 (file)
@@ -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
@@ -1103,6 +1115,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 +1127,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(),
@@ -1850,6 +1863,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()),
                };
@@ -2150,6 +2164,7 @@ mod tests {
                                                                channel_features: ChannelFeatures::empty(),
                                                                fee_msat: invoice.amount_msats(),
                                                                cltv_expiry_delta: 0,
+                                                               maybe_announced_channel: true,
                                                        }
                                                ],
                                                blinded_tail: None,
index d88730c22db058f6f39c0a0f140adc3fdc89dfb5..3def4e3629bf2fb98d227d9a95e5b1faf6493bc9 100644 (file)
@@ -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::<Vec<Option<UserConfig>>>();
+       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.
@@ -2201,6 +2298,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 +2307,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 +2321,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 +2330,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 +2344,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 +2589,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 +2598,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 +2680,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 +2767,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 +2775,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 +2784,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 +2792,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 +2971,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 +2979,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 +2988,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 +2996,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 +3139,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 +3147,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 +3156,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 +3164,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(
index 9c5fe8e1f9bd3dab59033644591a4a59b49d173b..1758cbbf9c3eaae24151050f488c2524f43109da 100644 (file)
@@ -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 {
index 26d555819d35c15e2f8aff22b0667d5e6ba49500..748edd31eaed679af2e6c5d767b9abd8139c25ec 100644 (file)
@@ -2186,6 +2186,7 @@ mod tests {
                        channel_features: channelmanager::provided_channel_features(&config),
                        fee_msat,
                        cltv_expiry_delta: 18,
+                       maybe_announced_channel: true,
                }
        }