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)) {
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(),
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)) {
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 });
// 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;
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;
/// 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)
}
/// [`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
///
/// 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,
///
/// 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())
}
}
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()
}
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.
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:
&'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
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>>(
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(),
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(),
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;
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(),
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(),
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;
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,
/// 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.
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
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);
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(),
channel_features: ChannelFeatures::empty(),
fee_msat: 0,
cltv_expiry_delta: 0,
+ maybe_announced_channel: true,
}], blinded_tail: None }],
route_params: Some(route_params.clone()),
};
channel_features: ChannelFeatures::empty(),
fee_msat: invoice.amount_msats(),
cltv_expiry_delta: 0,
+ maybe_announced_channel: true,
}
],
blinded_tail: None,
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;
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.
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(),
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()),
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(),
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()),
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()),
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(),
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()),
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(
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(),
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(),
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(),
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(
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(),
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(),
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(),
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(
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(),
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(),
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(),
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(
///
/// [`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),
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(),
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, _)| {
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,
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,
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,
channel_features: ChannelFeatures::empty(),
fee_msat: 100,
cltv_expiry_delta: 0,
+ maybe_announced_channel: true,
}], blinded_tail: None }],
route_params: None,
};
channel_features: ChannelFeatures::empty(),
fee_msat: 100,
cltv_expiry_delta: 0,
+ maybe_announced_channel: false,
},
RouteHop {
pubkey: blinded_path.introduction_node_id,
channel_features: ChannelFeatures::empty(),
fee_msat: 1,
cltv_expiry_delta: 0,
+ maybe_announced_channel: false,
}],
blinded_tail: Some(BlindedTail {
hops: blinded_path.blinded_hops,
channel_features: ChannelFeatures::empty(),
fee_msat: 100,
cltv_expiry_delta: 0,
+ maybe_announced_channel: false,
},
RouteHop {
pubkey: blinded_path.introduction_node_id,
channel_features: ChannelFeatures::empty(),
fee_msat: 1,
cltv_expiry_delta: 0,
+ maybe_announced_channel: false,
}
],
blinded_tail: Some(BlindedTail {
channel_features: channelmanager::provided_channel_features(&config),
fee_msat,
cltv_expiry_delta: 18,
+ maybe_announced_channel: true,
}
}