mirror of
https://github.com/lightningdevkit/rust-lightning.git
synced 2025-03-09 21:23:34 +01:00
Merge pull request #2534 from tnull/2023-08-upstream-preflight-probing
Upstream and fix preflight probing
This commit is contained in:
commit
36af1f06fa
10 changed files with 413 additions and 80 deletions
|
@ -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)) {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>>(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -2186,6 +2186,7 @@ mod tests {
|
|||
channel_features: channelmanager::provided_channel_features(&config),
|
||||
fee_msat,
|
||||
cltv_expiry_delta: 18,
|
||||
maybe_announced_channel: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue