Skip to content

Payment Retries - #1059

Merged
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry
Oct 27, 2021
Merged

Payment Retries#1059
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry

Conversation

@jkczyz

@jkczyzjkczyz commented Aug 24, 2021

Copy link
Copy Markdown
Contributor

When a payment fails, it's useful to retry the payment once the network graph and channel scores are updated. InvoicePayer is a utility for making payments which caches the payment Invoice so that it can be used when retrying the payment. It is parameterized by a Payer and Router for ease of customization and testing.

PaymentRetryHandler is used in connection with InvoicePayer. It is an EventHandler decorator that intercepts PaymentFailed events and retries that payment using the cached Invoice.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from ec4d0c2 to 910814fCompareAugust 24, 2021 14:53
@codecov

codecovBot commented Aug 24, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1059 (0386410) into main (2ed1ba6) will increase coverage by 0.18%.
The diff coverage is 91.54%.

❗ Current head 0386410 differs from pull request most recent head 010436d. Consider uploading reports for the commit 010436d to get more accurate results
Impacted file tree graph

@@ Coverage Diff @@## main #1059 +/- ##
==========================================
+ Coverage 90.47% 90.66% +0.18% 
==========================================
Files 68 67 -1 Lines 35273 35261 -12 ==========================================
+ Hits 31914 31969 +55 + Misses 3359 3292 -67 
Impacted FilesCoverage Δ
lightning/src/util/events.rs31.98% <12.50%> (-1.36%)⬇️
lightning-invoice/src/utils.rs72.64% <16.66%> (-11.45%)⬇️
lightning-invoice/src/payment.rs95.11% <95.11%> (ø)
lightning-invoice/src/lib.rs88.59% <96.15%> (+0.48%)⬆️
lightning-background-processor/src/lib.rs94.52% <100.00%> (+0.28%)⬆️
lightning/src/ln/channelmanager.rs84.87% <100.00%> (+0.91%)⬆️
lightning/src/ln/functional_tests.rs97.44% <100.00%> (+0.07%)⬆️
lightning/src/ln/onion_route_tests.rs96.64% <100.00%> (+0.04%)⬆️
lightning/src/routing/network_graph.rs91.29% <100.00%> (ø)
lightning/src/util/logger.rs83.14% <100.00%> (+1.00%)⬆️
... and 32 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ed1ba6...010436d. Read the comment docs.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment on lines +127 to +129
pub fn new<L: Deref, E: EventHandler>(
payer: P, router: R, network_graph: G, logger: L, event_handler: E
) -> (Arc<Self>, PaymentRetryHandler<P, R, G, L, E>)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that it would be better to separate the construction of InvoicePayer and PaymentRetryHandler, as it wouldn't require using an Arc while still allowing it.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 910814f to ae0caddCompareAugust 24, 2021 17:15

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, simple, clean API. I like it.

Comment threadlightning-invoice/src/utils.rs
Comment threadlightning-invoice/src/payment.rs Outdated
}

/// An [`EventHandler`] decorator for retrying failed payments.
pub struct PaymentRetryHandler<I, P: Deref, R, G, L: Deref, E>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand why this is separate from the Payer - can't we/shouldn't we just implement EventHandler on the Payer?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

As of now, the ownership model is such that any decorating EventHandler (or BackgroundProcessor for that matter) would take ownership of this, which means InvoicePayer could then only be used as an event handler and no longer for initiating payments. For instance, we'll need to decorate this with NetworkUpdateHandler so that updates are applied before retries.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this). But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

Err, yes.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this)

Yes, along this line is what I was suggesting.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise. Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise.

I mean my usual reply applies - do users ever have a reasonable use case where they'd want to ignore most events?

Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

I'm not entirely convinced that it's clear in the current api that you have to hook these things up in a given order, whether it says retry or not. I wonder if we should make it more clear? Maybe have this object pass the update to the network graph itself?

}
}
/// Logs a byte slice in hex format.
#[macro_export]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually #[doc(hidden)] this and all the other macros in this file that really shoulnd't be used by downstream crates but only LDK crates?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see log_internal needing that. But which other ones? Presumably users may want to log within their trait implementations, possibly using log_bytes, too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I was mostly figuring the logging stuff is a one-way street - the lightning crates log stuff, and users consume it and put it somewhere. I suppose there's no real reason to require that, though, so, ok, fair enough. Please do add it to the internal one, though.

Comment threadlightning-invoice/src/payment.rs Outdated
fn pay_cached_invoice(&self, payment_hash: &PaymentHash) -> Result<(), PaymentError> {
let invoice_cache = self.invoice_cache.lock().unwrap();
match invoice_cache.get(payment_hash) {
Some(invoice) => self.pay_invoice_internal(invoice),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we somehow check that the route used on a retry is different from the original? I could see users using this API and just hooking it up to a remote route-getter which doesn't care about our failed payments.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that for simplicity this could be route-agnostic. Any changes to the network or scoring could affect the route calculation, so we should let those modules (e.g., higher-level event handlers) take care of making adjustments for the next time a route is fetched, if necessary, even if it means the same route is tried.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away. Maybe we should expose the retry count instead to make it explicit for users?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away.

Well, three times... first attempt counts as one. :)

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Maybe we should expose the retry count instead to make it explicit for users?

On the other hand, that would be a good argument for merging InvoicePayer and PaymentRetryHandle given the number of attempts is stored on the latter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Hmm, true. I guess you wouldn't generally use the invoice utilities in this case? I wonder if we merge the two and make the NetworkGraph an optional part if we can then just do retries if we have one?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... may work, though now we start to affect the Router interface as it still needs a NetworkGraph for the first attempt. We could make it an Option in find_route. Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.

If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.
If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

I think I'm a fan of this, yea, it reduces extra wires to hook up, better captures the set of likely user use-cases and Does The Right Thing, or at least makes the right thing more obvious, in common cases.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 3061394 to 97bd88cCompareAugust 26, 2021 22:26
@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Sep 10, 2021
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 52da654 to aa6b426CompareSeptember 15, 2021 17:38
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase now 🎉.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 4c64c33 to 9c09290CompareSeptember 15, 2021 21:52

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, but I like this API. Note that I don't think this can go in a release without #1053, so should either land after that, or at least in the same release.

Comment threadlightning-invoice/src/payment.rs Outdated

// Either the payment was rejected, exceeded the maximum attempts, or failed retry.
self.remove_cached_invoice(payment_hash);
attempts_by_payment_hash.remove(payment_hash);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it seems somewhat duplicative to get the entry above and then remove it here. You could just store the entry and remove the entry here, doing the and_modify.or_insert in the retry condition before the return.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the number of attempts using and_modify and or_insert for the comparison, which consumes the entry. But if no retry is needed we, couldn't then remove the entry directly since it has been consumed.

Probably could just do and_modify then match to determine whether or not there's a value, but that doesn't seem as clean.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not quite as clean, but it does feel incredibly wasteful to hash into the hashmap twice for the same element 5 lines apart :/

--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -202,11 +202,13 @@ where
let mut attempts_by_payment_hash = self.payment_attempts.lock().unwrap();
- let attempts = attempts_by_payment_hash
- .entry(*payment_hash)
- .and_modify(|attempts| *attempts += 1)
- .or_insert(1);
+ let attempts_entry = attempts_by_payment_hash
+ .entry(*payment_hash);
+ let attempts = if let hash_map::Entry::Occupied(e) = &attempts_entry { e.get() + 1 } else { 1 };
if !rejected_by_dest {
let max_payment_attempts = self.retry_attempts + 1;
- if *attempts < max_payment_attempts {
+ if attempts < max_payment_attempts {
if self.pay_cached_invoice(payment_hash).is_ok() {
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
+ attempts_entry
+ .and_modify(|attempts| *attempts += 1)
+ .or_insert(1);
return;
@@ -224,3 +226,2 @@ where
self.remove_cached_invoice(payment_hash);
- attempts_by_payment_hash.remove(payment_hash);
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you still need to remove the entry later, but that does work. However, there's still an additional look-up when remove_cached_invoice is called. Given no tests failed when the attempts entry was not removed -- and that when called independently remove_cached_invoice won't remove the attempts entry -- it probably makes sense to combine these two hash maps to ensure consistency. Along with your improvement, that takes us to one look-up instead of three.

Comment threadlightning-background-processor/src/lib.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from a308452 to 41d2b07CompareSeptember 20, 2021 19:47
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c6e3c06 to 4caedcfCompareSeptember 21, 2021 04:57
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Once #1084 is merged, I can rebase and fix the failing checks.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can be rebased now :)

@jkczyzjkczyz mentioned this pull request Sep 27, 2021
4 tasks
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 4caedcf to 35ad581CompareSeptember 27, 2021 21:35
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Note we should consider the invoice expiry when/before retrying.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c7d2086 to 14f7d04CompareSeptember 30, 2021 21:14
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
return;
} else {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the log could be a bit confusing here, because instead of a payment failed error, you immediately get an error retrying payment error.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let me know if you have a suggestion for rewording.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 3 times, most recently from 2aca5b1 to e7c1481CompareOctober 26, 2021 14:06
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Right, feels like we'll need to rewrite a good chunk of InvoicePayer to be stateless, might as well do it here instead of landing a bunch of code then replacing it tomorrow.

Ok, this is now using the parameters from PaymentPathFailed, so the only state is payment_cache for retry attempts. It's currently keyed by payment_hash, but I could change this to payment_id. I don't recall if we had a reason to prefer one over the other.

Otherwise, the main changes are:

  • Unified the get_route interface into a single find_route function so that Router only needs one method
  • Stored invoice expiry data in Payee since the Invoice is no longer cached
  • Removed commit adding a proof-of-concept test to BackgroundProcessor

Comment threadlightning/src/util/ser.rs Outdated
}
}

impl Writeable for Duration {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can we just write out a u64 instead? It feels a bit funky serializing a duration and losing a bunch of precision as a general thing?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed as per other discussion. Could also keep Duration and serialized as u128 nanoseconds. That said, for our use case, BOLT 11 defines the expiry time in seconds, so no need for more precision.

Comment threadlightning/src/routing/router.rs Outdated
/// Hints for routing to the payee, containing channels connecting the payee to public nodes.
pub route_hints: Vec<RouteHint>,

/// Expiration of a payment to the payee, relative to a user-defined epoch.

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a "user-defined epoch"? How is this expected to work with InvoicePayer - doesn't it always check against 1970? Can we just make this seconds-since-1970 and a u64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thought was to let the user set this relative to whatever they wanted because ultimately they would be setting it and interpreting it. So even if we say it's seconds since the UNIX epoch, there's really nothing enforcing that.

Anyhow, updated to u64 as discussed offline.

Comment threadlightning-invoice/src/payment.rs Outdated
// error occurred when attempting to retry.
entry.remove();
} else {
log_trace!(self.logger, "Unknown payment {}; cannot retry)", log_bytes!(payment_hash.0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to auto-insert here (and maybe a test for retry on reload, but I'm happy to add that as a followup for you).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to handle this and added a unit test.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from e7c1481 to c241093CompareOctober 26, 2021 17:35
/// The id returned by [`ChannelManager::send_payment`] and used with
/// [`ChannelManager::retry_payment`].
///
/// [`ChannelManager::send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just noticed that documentation for payment_id seems to be missing from ChannelMannager::send_payment. It would be nice to (a) document that the id should be stored for a retry, and (b) that it's needed because technically PaymentHashs can be non-unique.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving unresolved pending discussions around changes to send_payment.

write_tlv_fields!(writer, {
(0, payment_preimage, required),
(1, payment_hash, required),
(3, payment_id, option),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be a little nicer public API-wise if payment_id were a non-Option, and set to 0 if serialized before 0.0.104 (or wtv). Similar for the next commit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we leave it an Option for now and then just do a hard-break on our serialization backwards compat in like 2-3 versions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me! 🙂

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, maybe that is why we decided to key by payment_hash for now? I guess we could just not retry if PaymentId is None, though.

Comment threadlightning-invoice/src/payment.rs Outdated
}

/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {

@valentinewallacevalentinewallaceOct 26, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I'd feel better about this if we had a plan for this, it does seem like a lot of rewrites would have to happen to add spontaneous support (and feels odd to merge code that'll be definitely rewritten)? From Slack chats, it looks like the relevant user has implemented their own retries for now

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to properly interpret PaymentSendFailure errors but otherwise looking good, I think.

Comment threadfuzz/src/full_stack.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
let payment_secret = Some(invoice.payment_secret().clone());
let payment_id = self.payer.send_payment(&route, payment_hash, &payment_secret)
.map_err(|e| PaymentError::Sending(e))?;

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to figure out what's up - if we get a PaymentSendFailure::ParameterError or PaymentSendFailure::PathParameterError then we can return an error here, I think. AllFailedRetrySafe we should retry the requisite numbers right here, PartialFailure we should insert and retry the relevant paths.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. This may require a bit more work since we lose the PaymentId if send_payment returns an error. Ideally, we'd get back the proper RouteParameters for retry.

} else if has_expired(retry.as_ref().unwrap()) {
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else if self.retry_payment(*payment_id.as_ref().unwrap(), retry.as_ref().unwrap()).is_err() {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not always imply failure to send at all - see PaymentSendFailure and the notes at PaymentSendFailure::PartialFailure about when retry is safe.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these cases (both returned by send_payment and retry_payment), I take it we must retry here as there won't be a PaymentPathFailed event. Though one idea would be to generate them. It would make the retry attempt logic cleaner at very least.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK with #1141. Happy to land this as-is, but #1141 will have to be included in the same release.

An upcoming Router interface will be used for finding a Route both when
initially sending a payment and also when retrying failed payment paths.
Unify the three varieties of get_route so the interface can consist of a
single method implemented by the new `find_route` method. Give get_route
pub(crate) visibility so it can still be used in tests.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2864f61 to 2042241CompareOctober 27, 2021 01:16
When a payment fails, it's useful to retry the payment once the network
graph and channel scores are updated. InvoicePayer is a utility for
making payments which will retry any failed payment paths for a payment
up to a configured number of total attempts. It is parameterized by a
Payer and Router for ease of customization and testing.
Implement EventHandler for InvoicePayer as a decorator that intercepts
PaymentPathFailed events and retries that payment using the parameters
from the event. It delegates to the decorated EventHandler after retries
have been exhausted and for other events.
According to BOLT 11:
- after the `timestamp` plus `expiry` has passed
- SHOULD NOT attempt a payment
Add a convenience method for checking if an Invoice has expired, and use
it to short-circuit payment retries.
Implements Payer for ChannelManager and Rotuer for find_route, which can
be used to parameterize InvoicePayer when needing payment retries.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2042241 to 010436dCompareOctober 27, 2021 15:59
@jkczyzjkczyz mentioned this pull request Oct 27, 2021
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Diff from previous ACKs is trivial test changes, taking:

$ git diff-tree -U3 2042241f8885c0a17740b3867db4364be9fff29d 010436d07
diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs
index f51a4e580..7e931d66f 100644
--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -921,9 +921,9 @@ mod tests {
fn fails_on_attempt(self, attempt: usize) -> Self {
Self {
+ expectations: core::cell::RefCell::new(self.expectations.borrow().clone()),
attempts: core::cell::RefCell::new(0),
failing_on_attempt: Some(attempt),
- ..self
}
}
@@ -946,6 +946,18 @@ mod tests {
}
}
+	impl Drop for TestPayer {
+ fn drop(&mut self) {
+ if std::thread::panicking() {
+ return;
+ }
+
+ if !self.expectations.borrow().is_empty() {
+ panic!("Unsatisfied payment expectations: {:?}", self.expectations.borrow());
+ }
+ }
+	}
+
impl Payer for TestPayer {
fn node_id(&self) -> PublicKey {
let secp_ctx = Secp256k1::new();
$

@TheBlueMatt
TheBlueMatt merged commit 59659d3 into lightningdevkit:mainOct 27, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jkczyz@TheBlueMatt@arik-so@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Payment Retries by jkczyz · Pull Request #1059 · lightningdevkit/rust-lightning · GitHub
Skip to content

Payment Retries - #1059

Merged
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry
Oct 27, 2021
Merged

Payment Retries#1059
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry

Conversation

@jkczyz

@jkczyzjkczyz commented Aug 24, 2021

Copy link
Copy Markdown
Contributor

When a payment fails, it's useful to retry the payment once the network graph and channel scores are updated. InvoicePayer is a utility for making payments which caches the payment Invoice so that it can be used when retrying the payment. It is parameterized by a Payer and Router for ease of customization and testing.

PaymentRetryHandler is used in connection with InvoicePayer. It is an EventHandler decorator that intercepts PaymentFailed events and retries that payment using the cached Invoice.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from ec4d0c2 to 910814fCompareAugust 24, 2021 14:53
@codecov

codecovBot commented Aug 24, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1059 (0386410) into main (2ed1ba6) will increase coverage by 0.18%.
The diff coverage is 91.54%.

❗ Current head 0386410 differs from pull request most recent head 010436d. Consider uploading reports for the commit 010436d to get more accurate results
Impacted file tree graph

@@ Coverage Diff @@## main #1059 +/- ##
==========================================
+ Coverage 90.47% 90.66% +0.18% 
==========================================
Files 68 67 -1 Lines 35273 35261 -12 ==========================================
+ Hits 31914 31969 +55 + Misses 3359 3292 -67 
Impacted FilesCoverage Δ
lightning/src/util/events.rs31.98% <12.50%> (-1.36%)⬇️
lightning-invoice/src/utils.rs72.64% <16.66%> (-11.45%)⬇️
lightning-invoice/src/payment.rs95.11% <95.11%> (ø)
lightning-invoice/src/lib.rs88.59% <96.15%> (+0.48%)⬆️
lightning-background-processor/src/lib.rs94.52% <100.00%> (+0.28%)⬆️
lightning/src/ln/channelmanager.rs84.87% <100.00%> (+0.91%)⬆️
lightning/src/ln/functional_tests.rs97.44% <100.00%> (+0.07%)⬆️
lightning/src/ln/onion_route_tests.rs96.64% <100.00%> (+0.04%)⬆️
lightning/src/routing/network_graph.rs91.29% <100.00%> (ø)
lightning/src/util/logger.rs83.14% <100.00%> (+1.00%)⬆️
... and 32 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ed1ba6...010436d. Read the comment docs.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment on lines +127 to +129
pub fn new<L: Deref, E: EventHandler>(
payer: P, router: R, network_graph: G, logger: L, event_handler: E
) -> (Arc<Self>, PaymentRetryHandler<P, R, G, L, E>)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that it would be better to separate the construction of InvoicePayer and PaymentRetryHandler, as it wouldn't require using an Arc while still allowing it.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 910814f to ae0caddCompareAugust 24, 2021 17:15

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, simple, clean API. I like it.

Comment threadlightning-invoice/src/utils.rs
Comment threadlightning-invoice/src/payment.rs Outdated
}

/// An [`EventHandler`] decorator for retrying failed payments.
pub struct PaymentRetryHandler<I, P: Deref, R, G, L: Deref, E>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand why this is separate from the Payer - can't we/shouldn't we just implement EventHandler on the Payer?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

As of now, the ownership model is such that any decorating EventHandler (or BackgroundProcessor for that matter) would take ownership of this, which means InvoicePayer could then only be used as an event handler and no longer for initiating payments. For instance, we'll need to decorate this with NetworkUpdateHandler so that updates are applied before retries.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this). But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

Err, yes.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this)

Yes, along this line is what I was suggesting.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise. Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise.

I mean my usual reply applies - do users ever have a reasonable use case where they'd want to ignore most events?

Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

I'm not entirely convinced that it's clear in the current api that you have to hook these things up in a given order, whether it says retry or not. I wonder if we should make it more clear? Maybe have this object pass the update to the network graph itself?

}
}
/// Logs a byte slice in hex format.
#[macro_export]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually #[doc(hidden)] this and all the other macros in this file that really shoulnd't be used by downstream crates but only LDK crates?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see log_internal needing that. But which other ones? Presumably users may want to log within their trait implementations, possibly using log_bytes, too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I was mostly figuring the logging stuff is a one-way street - the lightning crates log stuff, and users consume it and put it somewhere. I suppose there's no real reason to require that, though, so, ok, fair enough. Please do add it to the internal one, though.

Comment threadlightning-invoice/src/payment.rs Outdated
fn pay_cached_invoice(&self, payment_hash: &PaymentHash) -> Result<(), PaymentError> {
let invoice_cache = self.invoice_cache.lock().unwrap();
match invoice_cache.get(payment_hash) {
Some(invoice) => self.pay_invoice_internal(invoice),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we somehow check that the route used on a retry is different from the original? I could see users using this API and just hooking it up to a remote route-getter which doesn't care about our failed payments.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that for simplicity this could be route-agnostic. Any changes to the network or scoring could affect the route calculation, so we should let those modules (e.g., higher-level event handlers) take care of making adjustments for the next time a route is fetched, if necessary, even if it means the same route is tried.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away. Maybe we should expose the retry count instead to make it explicit for users?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away.

Well, three times... first attempt counts as one. :)

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Maybe we should expose the retry count instead to make it explicit for users?

On the other hand, that would be a good argument for merging InvoicePayer and PaymentRetryHandle given the number of attempts is stored on the latter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Hmm, true. I guess you wouldn't generally use the invoice utilities in this case? I wonder if we merge the two and make the NetworkGraph an optional part if we can then just do retries if we have one?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... may work, though now we start to affect the Router interface as it still needs a NetworkGraph for the first attempt. We could make it an Option in find_route. Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.

If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.
If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

I think I'm a fan of this, yea, it reduces extra wires to hook up, better captures the set of likely user use-cases and Does The Right Thing, or at least makes the right thing more obvious, in common cases.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 3061394 to 97bd88cCompareAugust 26, 2021 22:26
@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Sep 10, 2021
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 52da654 to aa6b426CompareSeptember 15, 2021 17:38
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase now 🎉.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 4c64c33 to 9c09290CompareSeptember 15, 2021 21:52

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, but I like this API. Note that I don't think this can go in a release without #1053, so should either land after that, or at least in the same release.

Comment threadlightning-invoice/src/payment.rs Outdated

// Either the payment was rejected, exceeded the maximum attempts, or failed retry.
self.remove_cached_invoice(payment_hash);
attempts_by_payment_hash.remove(payment_hash);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it seems somewhat duplicative to get the entry above and then remove it here. You could just store the entry and remove the entry here, doing the and_modify.or_insert in the retry condition before the return.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the number of attempts using and_modify and or_insert for the comparison, which consumes the entry. But if no retry is needed we, couldn't then remove the entry directly since it has been consumed.

Probably could just do and_modify then match to determine whether or not there's a value, but that doesn't seem as clean.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not quite as clean, but it does feel incredibly wasteful to hash into the hashmap twice for the same element 5 lines apart :/

--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -202,11 +202,13 @@ where
let mut attempts_by_payment_hash = self.payment_attempts.lock().unwrap();
- let attempts = attempts_by_payment_hash
- .entry(*payment_hash)
- .and_modify(|attempts| *attempts += 1)
- .or_insert(1);
+ let attempts_entry = attempts_by_payment_hash
+ .entry(*payment_hash);
+ let attempts = if let hash_map::Entry::Occupied(e) = &attempts_entry { e.get() + 1 } else { 1 };
if !rejected_by_dest {
let max_payment_attempts = self.retry_attempts + 1;
- if *attempts < max_payment_attempts {
+ if attempts < max_payment_attempts {
if self.pay_cached_invoice(payment_hash).is_ok() {
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
+ attempts_entry
+ .and_modify(|attempts| *attempts += 1)
+ .or_insert(1);
return;
@@ -224,3 +226,2 @@ where
self.remove_cached_invoice(payment_hash);
- attempts_by_payment_hash.remove(payment_hash);
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you still need to remove the entry later, but that does work. However, there's still an additional look-up when remove_cached_invoice is called. Given no tests failed when the attempts entry was not removed -- and that when called independently remove_cached_invoice won't remove the attempts entry -- it probably makes sense to combine these two hash maps to ensure consistency. Along with your improvement, that takes us to one look-up instead of three.

Comment threadlightning-background-processor/src/lib.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from a308452 to 41d2b07CompareSeptember 20, 2021 19:47
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c6e3c06 to 4caedcfCompareSeptember 21, 2021 04:57
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Once #1084 is merged, I can rebase and fix the failing checks.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can be rebased now :)

@jkczyzjkczyz mentioned this pull request Sep 27, 2021
4 tasks
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 4caedcf to 35ad581CompareSeptember 27, 2021 21:35
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Note we should consider the invoice expiry when/before retrying.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c7d2086 to 14f7d04CompareSeptember 30, 2021 21:14
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
return;
} else {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the log could be a bit confusing here, because instead of a payment failed error, you immediately get an error retrying payment error.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let me know if you have a suggestion for rewording.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 3 times, most recently from 2aca5b1 to e7c1481CompareOctober 26, 2021 14:06
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Right, feels like we'll need to rewrite a good chunk of InvoicePayer to be stateless, might as well do it here instead of landing a bunch of code then replacing it tomorrow.

Ok, this is now using the parameters from PaymentPathFailed, so the only state is payment_cache for retry attempts. It's currently keyed by payment_hash, but I could change this to payment_id. I don't recall if we had a reason to prefer one over the other.

Otherwise, the main changes are:

  • Unified the get_route interface into a single find_route function so that Router only needs one method
  • Stored invoice expiry data in Payee since the Invoice is no longer cached
  • Removed commit adding a proof-of-concept test to BackgroundProcessor

Comment threadlightning/src/util/ser.rs Outdated
}
}

impl Writeable for Duration {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can we just write out a u64 instead? It feels a bit funky serializing a duration and losing a bunch of precision as a general thing?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed as per other discussion. Could also keep Duration and serialized as u128 nanoseconds. That said, for our use case, BOLT 11 defines the expiry time in seconds, so no need for more precision.

Comment threadlightning/src/routing/router.rs Outdated
/// Hints for routing to the payee, containing channels connecting the payee to public nodes.
pub route_hints: Vec<RouteHint>,

/// Expiration of a payment to the payee, relative to a user-defined epoch.

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a "user-defined epoch"? How is this expected to work with InvoicePayer - doesn't it always check against 1970? Can we just make this seconds-since-1970 and a u64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thought was to let the user set this relative to whatever they wanted because ultimately they would be setting it and interpreting it. So even if we say it's seconds since the UNIX epoch, there's really nothing enforcing that.

Anyhow, updated to u64 as discussed offline.

Comment threadlightning-invoice/src/payment.rs Outdated
// error occurred when attempting to retry.
entry.remove();
} else {
log_trace!(self.logger, "Unknown payment {}; cannot retry)", log_bytes!(payment_hash.0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to auto-insert here (and maybe a test for retry on reload, but I'm happy to add that as a followup for you).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to handle this and added a unit test.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from e7c1481 to c241093CompareOctober 26, 2021 17:35
/// The id returned by [`ChannelManager::send_payment`] and used with
/// [`ChannelManager::retry_payment`].
///
/// [`ChannelManager::send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just noticed that documentation for payment_id seems to be missing from ChannelMannager::send_payment. It would be nice to (a) document that the id should be stored for a retry, and (b) that it's needed because technically PaymentHashs can be non-unique.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving unresolved pending discussions around changes to send_payment.

write_tlv_fields!(writer, {
(0, payment_preimage, required),
(1, payment_hash, required),
(3, payment_id, option),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be a little nicer public API-wise if payment_id were a non-Option, and set to 0 if serialized before 0.0.104 (or wtv). Similar for the next commit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we leave it an Option for now and then just do a hard-break on our serialization backwards compat in like 2-3 versions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me! 🙂

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, maybe that is why we decided to key by payment_hash for now? I guess we could just not retry if PaymentId is None, though.

Comment threadlightning-invoice/src/payment.rs Outdated
}

/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {

@valentinewallacevalentinewallaceOct 26, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I'd feel better about this if we had a plan for this, it does seem like a lot of rewrites would have to happen to add spontaneous support (and feels odd to merge code that'll be definitely rewritten)? From Slack chats, it looks like the relevant user has implemented their own retries for now

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to properly interpret PaymentSendFailure errors but otherwise looking good, I think.

Comment threadfuzz/src/full_stack.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
let payment_secret = Some(invoice.payment_secret().clone());
let payment_id = self.payer.send_payment(&route, payment_hash, &payment_secret)
.map_err(|e| PaymentError::Sending(e))?;

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to figure out what's up - if we get a PaymentSendFailure::ParameterError or PaymentSendFailure::PathParameterError then we can return an error here, I think. AllFailedRetrySafe we should retry the requisite numbers right here, PartialFailure we should insert and retry the relevant paths.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. This may require a bit more work since we lose the PaymentId if send_payment returns an error. Ideally, we'd get back the proper RouteParameters for retry.

} else if has_expired(retry.as_ref().unwrap()) {
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else if self.retry_payment(*payment_id.as_ref().unwrap(), retry.as_ref().unwrap()).is_err() {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not always imply failure to send at all - see PaymentSendFailure and the notes at PaymentSendFailure::PartialFailure about when retry is safe.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these cases (both returned by send_payment and retry_payment), I take it we must retry here as there won't be a PaymentPathFailed event. Though one idea would be to generate them. It would make the retry attempt logic cleaner at very least.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK with #1141. Happy to land this as-is, but #1141 will have to be included in the same release.

An upcoming Router interface will be used for finding a Route both when
initially sending a payment and also when retrying failed payment paths.
Unify the three varieties of get_route so the interface can consist of a
single method implemented by the new `find_route` method. Give get_route
pub(crate) visibility so it can still be used in tests.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2864f61 to 2042241CompareOctober 27, 2021 01:16
When a payment fails, it's useful to retry the payment once the network
graph and channel scores are updated. InvoicePayer is a utility for
making payments which will retry any failed payment paths for a payment
up to a configured number of total attempts. It is parameterized by a
Payer and Router for ease of customization and testing.
Implement EventHandler for InvoicePayer as a decorator that intercepts
PaymentPathFailed events and retries that payment using the parameters
from the event. It delegates to the decorated EventHandler after retries
have been exhausted and for other events.
According to BOLT 11:
- after the `timestamp` plus `expiry` has passed
- SHOULD NOT attempt a payment
Add a convenience method for checking if an Invoice has expired, and use
it to short-circuit payment retries.
Implements Payer for ChannelManager and Rotuer for find_route, which can
be used to parameterize InvoicePayer when needing payment retries.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2042241 to 010436dCompareOctober 27, 2021 15:59
@jkczyzjkczyz mentioned this pull request Oct 27, 2021
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Diff from previous ACKs is trivial test changes, taking:

$ git diff-tree -U3 2042241f8885c0a17740b3867db4364be9fff29d 010436d07
diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs
index f51a4e580..7e931d66f 100644
--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -921,9 +921,9 @@ mod tests {
fn fails_on_attempt(self, attempt: usize) -> Self {
Self {
+ expectations: core::cell::RefCell::new(self.expectations.borrow().clone()),
attempts: core::cell::RefCell::new(0),
failing_on_attempt: Some(attempt),
- ..self
}
}
@@ -946,6 +946,18 @@ mod tests {
}
}
+	impl Drop for TestPayer {
+ fn drop(&mut self) {
+ if std::thread::panicking() {
+ return;
+ }
+
+ if !self.expectations.borrow().is_empty() {
+ panic!("Unsatisfied payment expectations: {:?}", self.expectations.borrow());
+ }
+ }
+	}
+
impl Payer for TestPayer {
fn node_id(&self) -> PublicKey {
let secp_ctx = Secp256k1::new();
$

@TheBlueMatt
TheBlueMatt merged commit 59659d3 into lightningdevkit:mainOct 27, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jkczyz@TheBlueMatt@arik-so@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Payment Retries by jkczyz · Pull Request #1059 · lightningdevkit/rust-lightning · GitHub
Skip to content

Payment Retries - #1059

Merged
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry
Oct 27, 2021
Merged

Payment Retries#1059
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry

Conversation

@jkczyz

@jkczyzjkczyz commented Aug 24, 2021

Copy link
Copy Markdown
Contributor

When a payment fails, it's useful to retry the payment once the network graph and channel scores are updated. InvoicePayer is a utility for making payments which caches the payment Invoice so that it can be used when retrying the payment. It is parameterized by a Payer and Router for ease of customization and testing.

PaymentRetryHandler is used in connection with InvoicePayer. It is an EventHandler decorator that intercepts PaymentFailed events and retries that payment using the cached Invoice.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from ec4d0c2 to 910814fCompareAugust 24, 2021 14:53
@codecov

codecovBot commented Aug 24, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1059 (0386410) into main (2ed1ba6) will increase coverage by 0.18%.
The diff coverage is 91.54%.

❗ Current head 0386410 differs from pull request most recent head 010436d. Consider uploading reports for the commit 010436d to get more accurate results
Impacted file tree graph

@@ Coverage Diff @@## main #1059 +/- ##
==========================================
+ Coverage 90.47% 90.66% +0.18% 
==========================================
Files 68 67 -1 Lines 35273 35261 -12 ==========================================
+ Hits 31914 31969 +55 + Misses 3359 3292 -67 
Impacted FilesCoverage Δ
lightning/src/util/events.rs31.98% <12.50%> (-1.36%)⬇️
lightning-invoice/src/utils.rs72.64% <16.66%> (-11.45%)⬇️
lightning-invoice/src/payment.rs95.11% <95.11%> (ø)
lightning-invoice/src/lib.rs88.59% <96.15%> (+0.48%)⬆️
lightning-background-processor/src/lib.rs94.52% <100.00%> (+0.28%)⬆️
lightning/src/ln/channelmanager.rs84.87% <100.00%> (+0.91%)⬆️
lightning/src/ln/functional_tests.rs97.44% <100.00%> (+0.07%)⬆️
lightning/src/ln/onion_route_tests.rs96.64% <100.00%> (+0.04%)⬆️
lightning/src/routing/network_graph.rs91.29% <100.00%> (ø)
lightning/src/util/logger.rs83.14% <100.00%> (+1.00%)⬆️
... and 32 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ed1ba6...010436d. Read the comment docs.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment on lines +127 to +129
pub fn new<L: Deref, E: EventHandler>(
payer: P, router: R, network_graph: G, logger: L, event_handler: E
) -> (Arc<Self>, PaymentRetryHandler<P, R, G, L, E>)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that it would be better to separate the construction of InvoicePayer and PaymentRetryHandler, as it wouldn't require using an Arc while still allowing it.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 910814f to ae0caddCompareAugust 24, 2021 17:15

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, simple, clean API. I like it.

Comment threadlightning-invoice/src/utils.rs
Comment threadlightning-invoice/src/payment.rs Outdated
}

/// An [`EventHandler`] decorator for retrying failed payments.
pub struct PaymentRetryHandler<I, P: Deref, R, G, L: Deref, E>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand why this is separate from the Payer - can't we/shouldn't we just implement EventHandler on the Payer?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

As of now, the ownership model is such that any decorating EventHandler (or BackgroundProcessor for that matter) would take ownership of this, which means InvoicePayer could then only be used as an event handler and no longer for initiating payments. For instance, we'll need to decorate this with NetworkUpdateHandler so that updates are applied before retries.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this). But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

Err, yes.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this)

Yes, along this line is what I was suggesting.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise. Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise.

I mean my usual reply applies - do users ever have a reasonable use case where they'd want to ignore most events?

Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

I'm not entirely convinced that it's clear in the current api that you have to hook these things up in a given order, whether it says retry or not. I wonder if we should make it more clear? Maybe have this object pass the update to the network graph itself?

}
}
/// Logs a byte slice in hex format.
#[macro_export]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually #[doc(hidden)] this and all the other macros in this file that really shoulnd't be used by downstream crates but only LDK crates?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see log_internal needing that. But which other ones? Presumably users may want to log within their trait implementations, possibly using log_bytes, too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I was mostly figuring the logging stuff is a one-way street - the lightning crates log stuff, and users consume it and put it somewhere. I suppose there's no real reason to require that, though, so, ok, fair enough. Please do add it to the internal one, though.

Comment threadlightning-invoice/src/payment.rs Outdated
fn pay_cached_invoice(&self, payment_hash: &PaymentHash) -> Result<(), PaymentError> {
let invoice_cache = self.invoice_cache.lock().unwrap();
match invoice_cache.get(payment_hash) {
Some(invoice) => self.pay_invoice_internal(invoice),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we somehow check that the route used on a retry is different from the original? I could see users using this API and just hooking it up to a remote route-getter which doesn't care about our failed payments.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that for simplicity this could be route-agnostic. Any changes to the network or scoring could affect the route calculation, so we should let those modules (e.g., higher-level event handlers) take care of making adjustments for the next time a route is fetched, if necessary, even if it means the same route is tried.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away. Maybe we should expose the retry count instead to make it explicit for users?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away.

Well, three times... first attempt counts as one. :)

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Maybe we should expose the retry count instead to make it explicit for users?

On the other hand, that would be a good argument for merging InvoicePayer and PaymentRetryHandle given the number of attempts is stored on the latter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Hmm, true. I guess you wouldn't generally use the invoice utilities in this case? I wonder if we merge the two and make the NetworkGraph an optional part if we can then just do retries if we have one?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... may work, though now we start to affect the Router interface as it still needs a NetworkGraph for the first attempt. We could make it an Option in find_route. Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.

If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.
If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

I think I'm a fan of this, yea, it reduces extra wires to hook up, better captures the set of likely user use-cases and Does The Right Thing, or at least makes the right thing more obvious, in common cases.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 3061394 to 97bd88cCompareAugust 26, 2021 22:26
@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Sep 10, 2021
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 52da654 to aa6b426CompareSeptember 15, 2021 17:38
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase now 🎉.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 4c64c33 to 9c09290CompareSeptember 15, 2021 21:52

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, but I like this API. Note that I don't think this can go in a release without #1053, so should either land after that, or at least in the same release.

Comment threadlightning-invoice/src/payment.rs Outdated

// Either the payment was rejected, exceeded the maximum attempts, or failed retry.
self.remove_cached_invoice(payment_hash);
attempts_by_payment_hash.remove(payment_hash);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it seems somewhat duplicative to get the entry above and then remove it here. You could just store the entry and remove the entry here, doing the and_modify.or_insert in the retry condition before the return.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the number of attempts using and_modify and or_insert for the comparison, which consumes the entry. But if no retry is needed we, couldn't then remove the entry directly since it has been consumed.

Probably could just do and_modify then match to determine whether or not there's a value, but that doesn't seem as clean.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not quite as clean, but it does feel incredibly wasteful to hash into the hashmap twice for the same element 5 lines apart :/

--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -202,11 +202,13 @@ where
let mut attempts_by_payment_hash = self.payment_attempts.lock().unwrap();
- let attempts = attempts_by_payment_hash
- .entry(*payment_hash)
- .and_modify(|attempts| *attempts += 1)
- .or_insert(1);
+ let attempts_entry = attempts_by_payment_hash
+ .entry(*payment_hash);
+ let attempts = if let hash_map::Entry::Occupied(e) = &attempts_entry { e.get() + 1 } else { 1 };
if !rejected_by_dest {
let max_payment_attempts = self.retry_attempts + 1;
- if *attempts < max_payment_attempts {
+ if attempts < max_payment_attempts {
if self.pay_cached_invoice(payment_hash).is_ok() {
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
+ attempts_entry
+ .and_modify(|attempts| *attempts += 1)
+ .or_insert(1);
return;
@@ -224,3 +226,2 @@ where
self.remove_cached_invoice(payment_hash);
- attempts_by_payment_hash.remove(payment_hash);
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you still need to remove the entry later, but that does work. However, there's still an additional look-up when remove_cached_invoice is called. Given no tests failed when the attempts entry was not removed -- and that when called independently remove_cached_invoice won't remove the attempts entry -- it probably makes sense to combine these two hash maps to ensure consistency. Along with your improvement, that takes us to one look-up instead of three.

Comment threadlightning-background-processor/src/lib.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from a308452 to 41d2b07CompareSeptember 20, 2021 19:47
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c6e3c06 to 4caedcfCompareSeptember 21, 2021 04:57
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Once #1084 is merged, I can rebase and fix the failing checks.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can be rebased now :)

@jkczyzjkczyz mentioned this pull request Sep 27, 2021
4 tasks
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 4caedcf to 35ad581CompareSeptember 27, 2021 21:35
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Note we should consider the invoice expiry when/before retrying.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c7d2086 to 14f7d04CompareSeptember 30, 2021 21:14
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
return;
} else {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the log could be a bit confusing here, because instead of a payment failed error, you immediately get an error retrying payment error.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let me know if you have a suggestion for rewording.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 3 times, most recently from 2aca5b1 to e7c1481CompareOctober 26, 2021 14:06
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Right, feels like we'll need to rewrite a good chunk of InvoicePayer to be stateless, might as well do it here instead of landing a bunch of code then replacing it tomorrow.

Ok, this is now using the parameters from PaymentPathFailed, so the only state is payment_cache for retry attempts. It's currently keyed by payment_hash, but I could change this to payment_id. I don't recall if we had a reason to prefer one over the other.

Otherwise, the main changes are:

  • Unified the get_route interface into a single find_route function so that Router only needs one method
  • Stored invoice expiry data in Payee since the Invoice is no longer cached
  • Removed commit adding a proof-of-concept test to BackgroundProcessor

Comment threadlightning/src/util/ser.rs Outdated
}
}

impl Writeable for Duration {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can we just write out a u64 instead? It feels a bit funky serializing a duration and losing a bunch of precision as a general thing?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed as per other discussion. Could also keep Duration and serialized as u128 nanoseconds. That said, for our use case, BOLT 11 defines the expiry time in seconds, so no need for more precision.

Comment threadlightning/src/routing/router.rs Outdated
/// Hints for routing to the payee, containing channels connecting the payee to public nodes.
pub route_hints: Vec<RouteHint>,

/// Expiration of a payment to the payee, relative to a user-defined epoch.

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a "user-defined epoch"? How is this expected to work with InvoicePayer - doesn't it always check against 1970? Can we just make this seconds-since-1970 and a u64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thought was to let the user set this relative to whatever they wanted because ultimately they would be setting it and interpreting it. So even if we say it's seconds since the UNIX epoch, there's really nothing enforcing that.

Anyhow, updated to u64 as discussed offline.

Comment threadlightning-invoice/src/payment.rs Outdated
// error occurred when attempting to retry.
entry.remove();
} else {
log_trace!(self.logger, "Unknown payment {}; cannot retry)", log_bytes!(payment_hash.0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to auto-insert here (and maybe a test for retry on reload, but I'm happy to add that as a followup for you).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to handle this and added a unit test.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from e7c1481 to c241093CompareOctober 26, 2021 17:35
/// The id returned by [`ChannelManager::send_payment`] and used with
/// [`ChannelManager::retry_payment`].
///
/// [`ChannelManager::send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just noticed that documentation for payment_id seems to be missing from ChannelMannager::send_payment. It would be nice to (a) document that the id should be stored for a retry, and (b) that it's needed because technically PaymentHashs can be non-unique.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving unresolved pending discussions around changes to send_payment.

write_tlv_fields!(writer, {
(0, payment_preimage, required),
(1, payment_hash, required),
(3, payment_id, option),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be a little nicer public API-wise if payment_id were a non-Option, and set to 0 if serialized before 0.0.104 (or wtv). Similar for the next commit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we leave it an Option for now and then just do a hard-break on our serialization backwards compat in like 2-3 versions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me! 🙂

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, maybe that is why we decided to key by payment_hash for now? I guess we could just not retry if PaymentId is None, though.

Comment threadlightning-invoice/src/payment.rs Outdated
}

/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {

@valentinewallacevalentinewallaceOct 26, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I'd feel better about this if we had a plan for this, it does seem like a lot of rewrites would have to happen to add spontaneous support (and feels odd to merge code that'll be definitely rewritten)? From Slack chats, it looks like the relevant user has implemented their own retries for now

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to properly interpret PaymentSendFailure errors but otherwise looking good, I think.

Comment threadfuzz/src/full_stack.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
let payment_secret = Some(invoice.payment_secret().clone());
let payment_id = self.payer.send_payment(&route, payment_hash, &payment_secret)
.map_err(|e| PaymentError::Sending(e))?;

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to figure out what's up - if we get a PaymentSendFailure::ParameterError or PaymentSendFailure::PathParameterError then we can return an error here, I think. AllFailedRetrySafe we should retry the requisite numbers right here, PartialFailure we should insert and retry the relevant paths.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. This may require a bit more work since we lose the PaymentId if send_payment returns an error. Ideally, we'd get back the proper RouteParameters for retry.

} else if has_expired(retry.as_ref().unwrap()) {
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else if self.retry_payment(*payment_id.as_ref().unwrap(), retry.as_ref().unwrap()).is_err() {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not always imply failure to send at all - see PaymentSendFailure and the notes at PaymentSendFailure::PartialFailure about when retry is safe.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these cases (both returned by send_payment and retry_payment), I take it we must retry here as there won't be a PaymentPathFailed event. Though one idea would be to generate them. It would make the retry attempt logic cleaner at very least.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK with #1141. Happy to land this as-is, but #1141 will have to be included in the same release.

An upcoming Router interface will be used for finding a Route both when
initially sending a payment and also when retrying failed payment paths.
Unify the three varieties of get_route so the interface can consist of a
single method implemented by the new `find_route` method. Give get_route
pub(crate) visibility so it can still be used in tests.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2864f61 to 2042241CompareOctober 27, 2021 01:16
When a payment fails, it's useful to retry the payment once the network
graph and channel scores are updated. InvoicePayer is a utility for
making payments which will retry any failed payment paths for a payment
up to a configured number of total attempts. It is parameterized by a
Payer and Router for ease of customization and testing.
Implement EventHandler for InvoicePayer as a decorator that intercepts
PaymentPathFailed events and retries that payment using the parameters
from the event. It delegates to the decorated EventHandler after retries
have been exhausted and for other events.
According to BOLT 11:
- after the `timestamp` plus `expiry` has passed
- SHOULD NOT attempt a payment
Add a convenience method for checking if an Invoice has expired, and use
it to short-circuit payment retries.
Implements Payer for ChannelManager and Rotuer for find_route, which can
be used to parameterize InvoicePayer when needing payment retries.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2042241 to 010436dCompareOctober 27, 2021 15:59
@jkczyzjkczyz mentioned this pull request Oct 27, 2021
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Diff from previous ACKs is trivial test changes, taking:

$ git diff-tree -U3 2042241f8885c0a17740b3867db4364be9fff29d 010436d07
diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs
index f51a4e580..7e931d66f 100644
--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -921,9 +921,9 @@ mod tests {
fn fails_on_attempt(self, attempt: usize) -> Self {
Self {
+ expectations: core::cell::RefCell::new(self.expectations.borrow().clone()),
attempts: core::cell::RefCell::new(0),
failing_on_attempt: Some(attempt),
- ..self
}
}
@@ -946,6 +946,18 @@ mod tests {
}
}
+	impl Drop for TestPayer {
+ fn drop(&mut self) {
+ if std::thread::panicking() {
+ return;
+ }
+
+ if !self.expectations.borrow().is_empty() {
+ panic!("Unsatisfied payment expectations: {:?}", self.expectations.borrow());
+ }
+ }
+	}
+
impl Payer for TestPayer {
fn node_id(&self) -> PublicKey {
let secp_ctx = Secp256k1::new();
$

@TheBlueMatt
TheBlueMatt merged commit 59659d3 into lightningdevkit:mainOct 27, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jkczyz@TheBlueMatt@arik-so@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Payment Retries by jkczyz · Pull Request #1059 · lightningdevkit/rust-lightning · GitHub
Skip to content

Payment Retries - #1059

Merged
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry
Oct 27, 2021
Merged

Payment Retries#1059
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry

Conversation

@jkczyz

@jkczyzjkczyz commented Aug 24, 2021

Copy link
Copy Markdown
Contributor

When a payment fails, it's useful to retry the payment once the network graph and channel scores are updated. InvoicePayer is a utility for making payments which caches the payment Invoice so that it can be used when retrying the payment. It is parameterized by a Payer and Router for ease of customization and testing.

PaymentRetryHandler is used in connection with InvoicePayer. It is an EventHandler decorator that intercepts PaymentFailed events and retries that payment using the cached Invoice.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from ec4d0c2 to 910814fCompareAugust 24, 2021 14:53
@codecov

codecovBot commented Aug 24, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1059 (0386410) into main (2ed1ba6) will increase coverage by 0.18%.
The diff coverage is 91.54%.

❗ Current head 0386410 differs from pull request most recent head 010436d. Consider uploading reports for the commit 010436d to get more accurate results
Impacted file tree graph

@@ Coverage Diff @@## main #1059 +/- ##
==========================================
+ Coverage 90.47% 90.66% +0.18% 
==========================================
Files 68 67 -1 Lines 35273 35261 -12 ==========================================
+ Hits 31914 31969 +55 + Misses 3359 3292 -67 
Impacted FilesCoverage Δ
lightning/src/util/events.rs31.98% <12.50%> (-1.36%)⬇️
lightning-invoice/src/utils.rs72.64% <16.66%> (-11.45%)⬇️
lightning-invoice/src/payment.rs95.11% <95.11%> (ø)
lightning-invoice/src/lib.rs88.59% <96.15%> (+0.48%)⬆️
lightning-background-processor/src/lib.rs94.52% <100.00%> (+0.28%)⬆️
lightning/src/ln/channelmanager.rs84.87% <100.00%> (+0.91%)⬆️
lightning/src/ln/functional_tests.rs97.44% <100.00%> (+0.07%)⬆️
lightning/src/ln/onion_route_tests.rs96.64% <100.00%> (+0.04%)⬆️
lightning/src/routing/network_graph.rs91.29% <100.00%> (ø)
lightning/src/util/logger.rs83.14% <100.00%> (+1.00%)⬆️
... and 32 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ed1ba6...010436d. Read the comment docs.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment on lines +127 to +129
pub fn new<L: Deref, E: EventHandler>(
payer: P, router: R, network_graph: G, logger: L, event_handler: E
) -> (Arc<Self>, PaymentRetryHandler<P, R, G, L, E>)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that it would be better to separate the construction of InvoicePayer and PaymentRetryHandler, as it wouldn't require using an Arc while still allowing it.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 910814f to ae0caddCompareAugust 24, 2021 17:15

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, simple, clean API. I like it.

Comment threadlightning-invoice/src/utils.rs
Comment threadlightning-invoice/src/payment.rs Outdated
}

/// An [`EventHandler`] decorator for retrying failed payments.
pub struct PaymentRetryHandler<I, P: Deref, R, G, L: Deref, E>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand why this is separate from the Payer - can't we/shouldn't we just implement EventHandler on the Payer?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

As of now, the ownership model is such that any decorating EventHandler (or BackgroundProcessor for that matter) would take ownership of this, which means InvoicePayer could then only be used as an event handler and no longer for initiating payments. For instance, we'll need to decorate this with NetworkUpdateHandler so that updates are applied before retries.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this). But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

Err, yes.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this)

Yes, along this line is what I was suggesting.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise. Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise.

I mean my usual reply applies - do users ever have a reasonable use case where they'd want to ignore most events?

Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

I'm not entirely convinced that it's clear in the current api that you have to hook these things up in a given order, whether it says retry or not. I wonder if we should make it more clear? Maybe have this object pass the update to the network graph itself?

}
}
/// Logs a byte slice in hex format.
#[macro_export]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually #[doc(hidden)] this and all the other macros in this file that really shoulnd't be used by downstream crates but only LDK crates?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see log_internal needing that. But which other ones? Presumably users may want to log within their trait implementations, possibly using log_bytes, too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I was mostly figuring the logging stuff is a one-way street - the lightning crates log stuff, and users consume it and put it somewhere. I suppose there's no real reason to require that, though, so, ok, fair enough. Please do add it to the internal one, though.

Comment threadlightning-invoice/src/payment.rs Outdated
fn pay_cached_invoice(&self, payment_hash: &PaymentHash) -> Result<(), PaymentError> {
let invoice_cache = self.invoice_cache.lock().unwrap();
match invoice_cache.get(payment_hash) {
Some(invoice) => self.pay_invoice_internal(invoice),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we somehow check that the route used on a retry is different from the original? I could see users using this API and just hooking it up to a remote route-getter which doesn't care about our failed payments.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that for simplicity this could be route-agnostic. Any changes to the network or scoring could affect the route calculation, so we should let those modules (e.g., higher-level event handlers) take care of making adjustments for the next time a route is fetched, if necessary, even if it means the same route is tried.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away. Maybe we should expose the retry count instead to make it explicit for users?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away.

Well, three times... first attempt counts as one. :)

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Maybe we should expose the retry count instead to make it explicit for users?

On the other hand, that would be a good argument for merging InvoicePayer and PaymentRetryHandle given the number of attempts is stored on the latter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Hmm, true. I guess you wouldn't generally use the invoice utilities in this case? I wonder if we merge the two and make the NetworkGraph an optional part if we can then just do retries if we have one?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... may work, though now we start to affect the Router interface as it still needs a NetworkGraph for the first attempt. We could make it an Option in find_route. Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.

If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.
If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

I think I'm a fan of this, yea, it reduces extra wires to hook up, better captures the set of likely user use-cases and Does The Right Thing, or at least makes the right thing more obvious, in common cases.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 3061394 to 97bd88cCompareAugust 26, 2021 22:26
@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Sep 10, 2021
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 52da654 to aa6b426CompareSeptember 15, 2021 17:38
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase now 🎉.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 4c64c33 to 9c09290CompareSeptember 15, 2021 21:52

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, but I like this API. Note that I don't think this can go in a release without #1053, so should either land after that, or at least in the same release.

Comment threadlightning-invoice/src/payment.rs Outdated

// Either the payment was rejected, exceeded the maximum attempts, or failed retry.
self.remove_cached_invoice(payment_hash);
attempts_by_payment_hash.remove(payment_hash);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it seems somewhat duplicative to get the entry above and then remove it here. You could just store the entry and remove the entry here, doing the and_modify.or_insert in the retry condition before the return.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the number of attempts using and_modify and or_insert for the comparison, which consumes the entry. But if no retry is needed we, couldn't then remove the entry directly since it has been consumed.

Probably could just do and_modify then match to determine whether or not there's a value, but that doesn't seem as clean.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not quite as clean, but it does feel incredibly wasteful to hash into the hashmap twice for the same element 5 lines apart :/

--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -202,11 +202,13 @@ where
let mut attempts_by_payment_hash = self.payment_attempts.lock().unwrap();
- let attempts = attempts_by_payment_hash
- .entry(*payment_hash)
- .and_modify(|attempts| *attempts += 1)
- .or_insert(1);
+ let attempts_entry = attempts_by_payment_hash
+ .entry(*payment_hash);
+ let attempts = if let hash_map::Entry::Occupied(e) = &attempts_entry { e.get() + 1 } else { 1 };
if !rejected_by_dest {
let max_payment_attempts = self.retry_attempts + 1;
- if *attempts < max_payment_attempts {
+ if attempts < max_payment_attempts {
if self.pay_cached_invoice(payment_hash).is_ok() {
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
+ attempts_entry
+ .and_modify(|attempts| *attempts += 1)
+ .or_insert(1);
return;
@@ -224,3 +226,2 @@ where
self.remove_cached_invoice(payment_hash);
- attempts_by_payment_hash.remove(payment_hash);
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you still need to remove the entry later, but that does work. However, there's still an additional look-up when remove_cached_invoice is called. Given no tests failed when the attempts entry was not removed -- and that when called independently remove_cached_invoice won't remove the attempts entry -- it probably makes sense to combine these two hash maps to ensure consistency. Along with your improvement, that takes us to one look-up instead of three.

Comment threadlightning-background-processor/src/lib.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from a308452 to 41d2b07CompareSeptember 20, 2021 19:47
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c6e3c06 to 4caedcfCompareSeptember 21, 2021 04:57
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Once #1084 is merged, I can rebase and fix the failing checks.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can be rebased now :)

@jkczyzjkczyz mentioned this pull request Sep 27, 2021
4 tasks
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 4caedcf to 35ad581CompareSeptember 27, 2021 21:35
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Note we should consider the invoice expiry when/before retrying.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c7d2086 to 14f7d04CompareSeptember 30, 2021 21:14
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
return;
} else {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the log could be a bit confusing here, because instead of a payment failed error, you immediately get an error retrying payment error.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let me know if you have a suggestion for rewording.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 3 times, most recently from 2aca5b1 to e7c1481CompareOctober 26, 2021 14:06
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Right, feels like we'll need to rewrite a good chunk of InvoicePayer to be stateless, might as well do it here instead of landing a bunch of code then replacing it tomorrow.

Ok, this is now using the parameters from PaymentPathFailed, so the only state is payment_cache for retry attempts. It's currently keyed by payment_hash, but I could change this to payment_id. I don't recall if we had a reason to prefer one over the other.

Otherwise, the main changes are:

  • Unified the get_route interface into a single find_route function so that Router only needs one method
  • Stored invoice expiry data in Payee since the Invoice is no longer cached
  • Removed commit adding a proof-of-concept test to BackgroundProcessor

Comment threadlightning/src/util/ser.rs Outdated
}
}

impl Writeable for Duration {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can we just write out a u64 instead? It feels a bit funky serializing a duration and losing a bunch of precision as a general thing?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed as per other discussion. Could also keep Duration and serialized as u128 nanoseconds. That said, for our use case, BOLT 11 defines the expiry time in seconds, so no need for more precision.

Comment threadlightning/src/routing/router.rs Outdated
/// Hints for routing to the payee, containing channels connecting the payee to public nodes.
pub route_hints: Vec<RouteHint>,

/// Expiration of a payment to the payee, relative to a user-defined epoch.

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a "user-defined epoch"? How is this expected to work with InvoicePayer - doesn't it always check against 1970? Can we just make this seconds-since-1970 and a u64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thought was to let the user set this relative to whatever they wanted because ultimately they would be setting it and interpreting it. So even if we say it's seconds since the UNIX epoch, there's really nothing enforcing that.

Anyhow, updated to u64 as discussed offline.

Comment threadlightning-invoice/src/payment.rs Outdated
// error occurred when attempting to retry.
entry.remove();
} else {
log_trace!(self.logger, "Unknown payment {}; cannot retry)", log_bytes!(payment_hash.0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to auto-insert here (and maybe a test for retry on reload, but I'm happy to add that as a followup for you).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to handle this and added a unit test.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from e7c1481 to c241093CompareOctober 26, 2021 17:35
/// The id returned by [`ChannelManager::send_payment`] and used with
/// [`ChannelManager::retry_payment`].
///
/// [`ChannelManager::send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just noticed that documentation for payment_id seems to be missing from ChannelMannager::send_payment. It would be nice to (a) document that the id should be stored for a retry, and (b) that it's needed because technically PaymentHashs can be non-unique.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving unresolved pending discussions around changes to send_payment.

write_tlv_fields!(writer, {
(0, payment_preimage, required),
(1, payment_hash, required),
(3, payment_id, option),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be a little nicer public API-wise if payment_id were a non-Option, and set to 0 if serialized before 0.0.104 (or wtv). Similar for the next commit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we leave it an Option for now and then just do a hard-break on our serialization backwards compat in like 2-3 versions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me! 🙂

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, maybe that is why we decided to key by payment_hash for now? I guess we could just not retry if PaymentId is None, though.

Comment threadlightning-invoice/src/payment.rs Outdated
}

/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {

@valentinewallacevalentinewallaceOct 26, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I'd feel better about this if we had a plan for this, it does seem like a lot of rewrites would have to happen to add spontaneous support (and feels odd to merge code that'll be definitely rewritten)? From Slack chats, it looks like the relevant user has implemented their own retries for now

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to properly interpret PaymentSendFailure errors but otherwise looking good, I think.

Comment threadfuzz/src/full_stack.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
let payment_secret = Some(invoice.payment_secret().clone());
let payment_id = self.payer.send_payment(&route, payment_hash, &payment_secret)
.map_err(|e| PaymentError::Sending(e))?;

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to figure out what's up - if we get a PaymentSendFailure::ParameterError or PaymentSendFailure::PathParameterError then we can return an error here, I think. AllFailedRetrySafe we should retry the requisite numbers right here, PartialFailure we should insert and retry the relevant paths.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. This may require a bit more work since we lose the PaymentId if send_payment returns an error. Ideally, we'd get back the proper RouteParameters for retry.

} else if has_expired(retry.as_ref().unwrap()) {
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else if self.retry_payment(*payment_id.as_ref().unwrap(), retry.as_ref().unwrap()).is_err() {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not always imply failure to send at all - see PaymentSendFailure and the notes at PaymentSendFailure::PartialFailure about when retry is safe.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these cases (both returned by send_payment and retry_payment), I take it we must retry here as there won't be a PaymentPathFailed event. Though one idea would be to generate them. It would make the retry attempt logic cleaner at very least.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK with #1141. Happy to land this as-is, but #1141 will have to be included in the same release.

An upcoming Router interface will be used for finding a Route both when
initially sending a payment and also when retrying failed payment paths.
Unify the three varieties of get_route so the interface can consist of a
single method implemented by the new `find_route` method. Give get_route
pub(crate) visibility so it can still be used in tests.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2864f61 to 2042241CompareOctober 27, 2021 01:16
When a payment fails, it's useful to retry the payment once the network
graph and channel scores are updated. InvoicePayer is a utility for
making payments which will retry any failed payment paths for a payment
up to a configured number of total attempts. It is parameterized by a
Payer and Router for ease of customization and testing.
Implement EventHandler for InvoicePayer as a decorator that intercepts
PaymentPathFailed events and retries that payment using the parameters
from the event. It delegates to the decorated EventHandler after retries
have been exhausted and for other events.
According to BOLT 11:
- after the `timestamp` plus `expiry` has passed
- SHOULD NOT attempt a payment
Add a convenience method for checking if an Invoice has expired, and use
it to short-circuit payment retries.
Implements Payer for ChannelManager and Rotuer for find_route, which can
be used to parameterize InvoicePayer when needing payment retries.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2042241 to 010436dCompareOctober 27, 2021 15:59
@jkczyzjkczyz mentioned this pull request Oct 27, 2021
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Diff from previous ACKs is trivial test changes, taking:

$ git diff-tree -U3 2042241f8885c0a17740b3867db4364be9fff29d 010436d07
diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs
index f51a4e580..7e931d66f 100644
--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -921,9 +921,9 @@ mod tests {
fn fails_on_attempt(self, attempt: usize) -> Self {
Self {
+ expectations: core::cell::RefCell::new(self.expectations.borrow().clone()),
attempts: core::cell::RefCell::new(0),
failing_on_attempt: Some(attempt),
- ..self
}
}
@@ -946,6 +946,18 @@ mod tests {
}
}
+	impl Drop for TestPayer {
+ fn drop(&mut self) {
+ if std::thread::panicking() {
+ return;
+ }
+
+ if !self.expectations.borrow().is_empty() {
+ panic!("Unsatisfied payment expectations: {:?}", self.expectations.borrow());
+ }
+ }
+	}
+
impl Payer for TestPayer {
fn node_id(&self) -> PublicKey {
let secp_ctx = Secp256k1::new();
$

@TheBlueMatt
TheBlueMatt merged commit 59659d3 into lightningdevkit:mainOct 27, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jkczyz@TheBlueMatt@arik-so@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Payment Retries by jkczyz · Pull Request #1059 · lightningdevkit/rust-lightning · GitHub
Skip to content

Payment Retries - #1059

Merged
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry
Oct 27, 2021
Merged

Payment Retries#1059
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry

Conversation

@jkczyz

@jkczyzjkczyz commented Aug 24, 2021

Copy link
Copy Markdown
Contributor

When a payment fails, it's useful to retry the payment once the network graph and channel scores are updated. InvoicePayer is a utility for making payments which caches the payment Invoice so that it can be used when retrying the payment. It is parameterized by a Payer and Router for ease of customization and testing.

PaymentRetryHandler is used in connection with InvoicePayer. It is an EventHandler decorator that intercepts PaymentFailed events and retries that payment using the cached Invoice.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from ec4d0c2 to 910814fCompareAugust 24, 2021 14:53
@codecov

codecovBot commented Aug 24, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1059 (0386410) into main (2ed1ba6) will increase coverage by 0.18%.
The diff coverage is 91.54%.

❗ Current head 0386410 differs from pull request most recent head 010436d. Consider uploading reports for the commit 010436d to get more accurate results
Impacted file tree graph

@@ Coverage Diff @@## main #1059 +/- ##
==========================================
+ Coverage 90.47% 90.66% +0.18% 
==========================================
Files 68 67 -1 Lines 35273 35261 -12 ==========================================
+ Hits 31914 31969 +55 + Misses 3359 3292 -67 
Impacted FilesCoverage Δ
lightning/src/util/events.rs31.98% <12.50%> (-1.36%)⬇️
lightning-invoice/src/utils.rs72.64% <16.66%> (-11.45%)⬇️
lightning-invoice/src/payment.rs95.11% <95.11%> (ø)
lightning-invoice/src/lib.rs88.59% <96.15%> (+0.48%)⬆️
lightning-background-processor/src/lib.rs94.52% <100.00%> (+0.28%)⬆️
lightning/src/ln/channelmanager.rs84.87% <100.00%> (+0.91%)⬆️
lightning/src/ln/functional_tests.rs97.44% <100.00%> (+0.07%)⬆️
lightning/src/ln/onion_route_tests.rs96.64% <100.00%> (+0.04%)⬆️
lightning/src/routing/network_graph.rs91.29% <100.00%> (ø)
lightning/src/util/logger.rs83.14% <100.00%> (+1.00%)⬆️
... and 32 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ed1ba6...010436d. Read the comment docs.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment on lines +127 to +129
pub fn new<L: Deref, E: EventHandler>(
payer: P, router: R, network_graph: G, logger: L, event_handler: E
) -> (Arc<Self>, PaymentRetryHandler<P, R, G, L, E>)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that it would be better to separate the construction of InvoicePayer and PaymentRetryHandler, as it wouldn't require using an Arc while still allowing it.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 910814f to ae0caddCompareAugust 24, 2021 17:15

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, simple, clean API. I like it.

Comment threadlightning-invoice/src/utils.rs
Comment threadlightning-invoice/src/payment.rs Outdated
}

/// An [`EventHandler`] decorator for retrying failed payments.
pub struct PaymentRetryHandler<I, P: Deref, R, G, L: Deref, E>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand why this is separate from the Payer - can't we/shouldn't we just implement EventHandler on the Payer?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

As of now, the ownership model is such that any decorating EventHandler (or BackgroundProcessor for that matter) would take ownership of this, which means InvoicePayer could then only be used as an event handler and no longer for initiating payments. For instance, we'll need to decorate this with NetworkUpdateHandler so that updates are applied before retries.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this). But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

Err, yes.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this)

Yes, along this line is what I was suggesting.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise. Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise.

I mean my usual reply applies - do users ever have a reasonable use case where they'd want to ignore most events?

Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

I'm not entirely convinced that it's clear in the current api that you have to hook these things up in a given order, whether it says retry or not. I wonder if we should make it more clear? Maybe have this object pass the update to the network graph itself?

}
}
/// Logs a byte slice in hex format.
#[macro_export]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually #[doc(hidden)] this and all the other macros in this file that really shoulnd't be used by downstream crates but only LDK crates?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see log_internal needing that. But which other ones? Presumably users may want to log within their trait implementations, possibly using log_bytes, too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I was mostly figuring the logging stuff is a one-way street - the lightning crates log stuff, and users consume it and put it somewhere. I suppose there's no real reason to require that, though, so, ok, fair enough. Please do add it to the internal one, though.

Comment threadlightning-invoice/src/payment.rs Outdated
fn pay_cached_invoice(&self, payment_hash: &PaymentHash) -> Result<(), PaymentError> {
let invoice_cache = self.invoice_cache.lock().unwrap();
match invoice_cache.get(payment_hash) {
Some(invoice) => self.pay_invoice_internal(invoice),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we somehow check that the route used on a retry is different from the original? I could see users using this API and just hooking it up to a remote route-getter which doesn't care about our failed payments.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that for simplicity this could be route-agnostic. Any changes to the network or scoring could affect the route calculation, so we should let those modules (e.g., higher-level event handlers) take care of making adjustments for the next time a route is fetched, if necessary, even if it means the same route is tried.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away. Maybe we should expose the retry count instead to make it explicit for users?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away.

Well, three times... first attempt counts as one. :)

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Maybe we should expose the retry count instead to make it explicit for users?

On the other hand, that would be a good argument for merging InvoicePayer and PaymentRetryHandle given the number of attempts is stored on the latter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Hmm, true. I guess you wouldn't generally use the invoice utilities in this case? I wonder if we merge the two and make the NetworkGraph an optional part if we can then just do retries if we have one?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... may work, though now we start to affect the Router interface as it still needs a NetworkGraph for the first attempt. We could make it an Option in find_route. Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.

If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.
If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

I think I'm a fan of this, yea, it reduces extra wires to hook up, better captures the set of likely user use-cases and Does The Right Thing, or at least makes the right thing more obvious, in common cases.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 3061394 to 97bd88cCompareAugust 26, 2021 22:26
@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Sep 10, 2021
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 52da654 to aa6b426CompareSeptember 15, 2021 17:38
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase now 🎉.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 4c64c33 to 9c09290CompareSeptember 15, 2021 21:52

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, but I like this API. Note that I don't think this can go in a release without #1053, so should either land after that, or at least in the same release.

Comment threadlightning-invoice/src/payment.rs Outdated

// Either the payment was rejected, exceeded the maximum attempts, or failed retry.
self.remove_cached_invoice(payment_hash);
attempts_by_payment_hash.remove(payment_hash);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it seems somewhat duplicative to get the entry above and then remove it here. You could just store the entry and remove the entry here, doing the and_modify.or_insert in the retry condition before the return.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the number of attempts using and_modify and or_insert for the comparison, which consumes the entry. But if no retry is needed we, couldn't then remove the entry directly since it has been consumed.

Probably could just do and_modify then match to determine whether or not there's a value, but that doesn't seem as clean.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not quite as clean, but it does feel incredibly wasteful to hash into the hashmap twice for the same element 5 lines apart :/

--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -202,11 +202,13 @@ where
let mut attempts_by_payment_hash = self.payment_attempts.lock().unwrap();
- let attempts = attempts_by_payment_hash
- .entry(*payment_hash)
- .and_modify(|attempts| *attempts += 1)
- .or_insert(1);
+ let attempts_entry = attempts_by_payment_hash
+ .entry(*payment_hash);
+ let attempts = if let hash_map::Entry::Occupied(e) = &attempts_entry { e.get() + 1 } else { 1 };
if !rejected_by_dest {
let max_payment_attempts = self.retry_attempts + 1;
- if *attempts < max_payment_attempts {
+ if attempts < max_payment_attempts {
if self.pay_cached_invoice(payment_hash).is_ok() {
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
+ attempts_entry
+ .and_modify(|attempts| *attempts += 1)
+ .or_insert(1);
return;
@@ -224,3 +226,2 @@ where
self.remove_cached_invoice(payment_hash);
- attempts_by_payment_hash.remove(payment_hash);
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you still need to remove the entry later, but that does work. However, there's still an additional look-up when remove_cached_invoice is called. Given no tests failed when the attempts entry was not removed -- and that when called independently remove_cached_invoice won't remove the attempts entry -- it probably makes sense to combine these two hash maps to ensure consistency. Along with your improvement, that takes us to one look-up instead of three.

Comment threadlightning-background-processor/src/lib.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from a308452 to 41d2b07CompareSeptember 20, 2021 19:47
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c6e3c06 to 4caedcfCompareSeptember 21, 2021 04:57
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Once #1084 is merged, I can rebase and fix the failing checks.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can be rebased now :)

@jkczyzjkczyz mentioned this pull request Sep 27, 2021
4 tasks
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 4caedcf to 35ad581CompareSeptember 27, 2021 21:35
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Note we should consider the invoice expiry when/before retrying.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c7d2086 to 14f7d04CompareSeptember 30, 2021 21:14
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
return;
} else {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the log could be a bit confusing here, because instead of a payment failed error, you immediately get an error retrying payment error.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let me know if you have a suggestion for rewording.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 3 times, most recently from 2aca5b1 to e7c1481CompareOctober 26, 2021 14:06
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Right, feels like we'll need to rewrite a good chunk of InvoicePayer to be stateless, might as well do it here instead of landing a bunch of code then replacing it tomorrow.

Ok, this is now using the parameters from PaymentPathFailed, so the only state is payment_cache for retry attempts. It's currently keyed by payment_hash, but I could change this to payment_id. I don't recall if we had a reason to prefer one over the other.

Otherwise, the main changes are:

  • Unified the get_route interface into a single find_route function so that Router only needs one method
  • Stored invoice expiry data in Payee since the Invoice is no longer cached
  • Removed commit adding a proof-of-concept test to BackgroundProcessor

Comment threadlightning/src/util/ser.rs Outdated
}
}

impl Writeable for Duration {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can we just write out a u64 instead? It feels a bit funky serializing a duration and losing a bunch of precision as a general thing?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed as per other discussion. Could also keep Duration and serialized as u128 nanoseconds. That said, for our use case, BOLT 11 defines the expiry time in seconds, so no need for more precision.

Comment threadlightning/src/routing/router.rs Outdated
/// Hints for routing to the payee, containing channels connecting the payee to public nodes.
pub route_hints: Vec<RouteHint>,

/// Expiration of a payment to the payee, relative to a user-defined epoch.

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a "user-defined epoch"? How is this expected to work with InvoicePayer - doesn't it always check against 1970? Can we just make this seconds-since-1970 and a u64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thought was to let the user set this relative to whatever they wanted because ultimately they would be setting it and interpreting it. So even if we say it's seconds since the UNIX epoch, there's really nothing enforcing that.

Anyhow, updated to u64 as discussed offline.

Comment threadlightning-invoice/src/payment.rs Outdated
// error occurred when attempting to retry.
entry.remove();
} else {
log_trace!(self.logger, "Unknown payment {}; cannot retry)", log_bytes!(payment_hash.0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to auto-insert here (and maybe a test for retry on reload, but I'm happy to add that as a followup for you).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to handle this and added a unit test.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from e7c1481 to c241093CompareOctober 26, 2021 17:35
/// The id returned by [`ChannelManager::send_payment`] and used with
/// [`ChannelManager::retry_payment`].
///
/// [`ChannelManager::send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just noticed that documentation for payment_id seems to be missing from ChannelMannager::send_payment. It would be nice to (a) document that the id should be stored for a retry, and (b) that it's needed because technically PaymentHashs can be non-unique.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving unresolved pending discussions around changes to send_payment.

write_tlv_fields!(writer, {
(0, payment_preimage, required),
(1, payment_hash, required),
(3, payment_id, option),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be a little nicer public API-wise if payment_id were a non-Option, and set to 0 if serialized before 0.0.104 (or wtv). Similar for the next commit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we leave it an Option for now and then just do a hard-break on our serialization backwards compat in like 2-3 versions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me! 🙂

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, maybe that is why we decided to key by payment_hash for now? I guess we could just not retry if PaymentId is None, though.

Comment threadlightning-invoice/src/payment.rs Outdated
}

/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {

@valentinewallacevalentinewallaceOct 26, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I'd feel better about this if we had a plan for this, it does seem like a lot of rewrites would have to happen to add spontaneous support (and feels odd to merge code that'll be definitely rewritten)? From Slack chats, it looks like the relevant user has implemented their own retries for now

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to properly interpret PaymentSendFailure errors but otherwise looking good, I think.

Comment threadfuzz/src/full_stack.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
let payment_secret = Some(invoice.payment_secret().clone());
let payment_id = self.payer.send_payment(&route, payment_hash, &payment_secret)
.map_err(|e| PaymentError::Sending(e))?;

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to figure out what's up - if we get a PaymentSendFailure::ParameterError or PaymentSendFailure::PathParameterError then we can return an error here, I think. AllFailedRetrySafe we should retry the requisite numbers right here, PartialFailure we should insert and retry the relevant paths.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. This may require a bit more work since we lose the PaymentId if send_payment returns an error. Ideally, we'd get back the proper RouteParameters for retry.

} else if has_expired(retry.as_ref().unwrap()) {
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else if self.retry_payment(*payment_id.as_ref().unwrap(), retry.as_ref().unwrap()).is_err() {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not always imply failure to send at all - see PaymentSendFailure and the notes at PaymentSendFailure::PartialFailure about when retry is safe.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these cases (both returned by send_payment and retry_payment), I take it we must retry here as there won't be a PaymentPathFailed event. Though one idea would be to generate them. It would make the retry attempt logic cleaner at very least.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK with #1141. Happy to land this as-is, but #1141 will have to be included in the same release.

An upcoming Router interface will be used for finding a Route both when
initially sending a payment and also when retrying failed payment paths.
Unify the three varieties of get_route so the interface can consist of a
single method implemented by the new `find_route` method. Give get_route
pub(crate) visibility so it can still be used in tests.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2864f61 to 2042241CompareOctober 27, 2021 01:16
When a payment fails, it's useful to retry the payment once the network
graph and channel scores are updated. InvoicePayer is a utility for
making payments which will retry any failed payment paths for a payment
up to a configured number of total attempts. It is parameterized by a
Payer and Router for ease of customization and testing.
Implement EventHandler for InvoicePayer as a decorator that intercepts
PaymentPathFailed events and retries that payment using the parameters
from the event. It delegates to the decorated EventHandler after retries
have been exhausted and for other events.
According to BOLT 11:
- after the `timestamp` plus `expiry` has passed
- SHOULD NOT attempt a payment
Add a convenience method for checking if an Invoice has expired, and use
it to short-circuit payment retries.
Implements Payer for ChannelManager and Rotuer for find_route, which can
be used to parameterize InvoicePayer when needing payment retries.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2042241 to 010436dCompareOctober 27, 2021 15:59
@jkczyzjkczyz mentioned this pull request Oct 27, 2021
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Diff from previous ACKs is trivial test changes, taking:

$ git diff-tree -U3 2042241f8885c0a17740b3867db4364be9fff29d 010436d07
diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs
index f51a4e580..7e931d66f 100644
--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -921,9 +921,9 @@ mod tests {
fn fails_on_attempt(self, attempt: usize) -> Self {
Self {
+ expectations: core::cell::RefCell::new(self.expectations.borrow().clone()),
attempts: core::cell::RefCell::new(0),
failing_on_attempt: Some(attempt),
- ..self
}
}
@@ -946,6 +946,18 @@ mod tests {
}
}
+	impl Drop for TestPayer {
+ fn drop(&mut self) {
+ if std::thread::panicking() {
+ return;
+ }
+
+ if !self.expectations.borrow().is_empty() {
+ panic!("Unsatisfied payment expectations: {:?}", self.expectations.borrow());
+ }
+ }
+	}
+
impl Payer for TestPayer {
fn node_id(&self) -> PublicKey {
let secp_ctx = Secp256k1::new();
$

@TheBlueMatt
TheBlueMatt merged commit 59659d3 into lightningdevkit:mainOct 27, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jkczyz@TheBlueMatt@arik-so@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Payment Retries by jkczyz · Pull Request #1059 · lightningdevkit/rust-lightning · GitHub
Skip to content

Payment Retries - #1059

Merged
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry
Oct 27, 2021
Merged

Payment Retries#1059
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry

Conversation

@jkczyz

@jkczyzjkczyz commented Aug 24, 2021

Copy link
Copy Markdown
Contributor

When a payment fails, it's useful to retry the payment once the network graph and channel scores are updated. InvoicePayer is a utility for making payments which caches the payment Invoice so that it can be used when retrying the payment. It is parameterized by a Payer and Router for ease of customization and testing.

PaymentRetryHandler is used in connection with InvoicePayer. It is an EventHandler decorator that intercepts PaymentFailed events and retries that payment using the cached Invoice.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from ec4d0c2 to 910814fCompareAugust 24, 2021 14:53
@codecov

codecovBot commented Aug 24, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1059 (0386410) into main (2ed1ba6) will increase coverage by 0.18%.
The diff coverage is 91.54%.

❗ Current head 0386410 differs from pull request most recent head 010436d. Consider uploading reports for the commit 010436d to get more accurate results
Impacted file tree graph

@@ Coverage Diff @@## main #1059 +/- ##
==========================================
+ Coverage 90.47% 90.66% +0.18% 
==========================================
Files 68 67 -1 Lines 35273 35261 -12 ==========================================
+ Hits 31914 31969 +55 + Misses 3359 3292 -67 
Impacted FilesCoverage Δ
lightning/src/util/events.rs31.98% <12.50%> (-1.36%)⬇️
lightning-invoice/src/utils.rs72.64% <16.66%> (-11.45%)⬇️
lightning-invoice/src/payment.rs95.11% <95.11%> (ø)
lightning-invoice/src/lib.rs88.59% <96.15%> (+0.48%)⬆️
lightning-background-processor/src/lib.rs94.52% <100.00%> (+0.28%)⬆️
lightning/src/ln/channelmanager.rs84.87% <100.00%> (+0.91%)⬆️
lightning/src/ln/functional_tests.rs97.44% <100.00%> (+0.07%)⬆️
lightning/src/ln/onion_route_tests.rs96.64% <100.00%> (+0.04%)⬆️
lightning/src/routing/network_graph.rs91.29% <100.00%> (ø)
lightning/src/util/logger.rs83.14% <100.00%> (+1.00%)⬆️
... and 32 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ed1ba6...010436d. Read the comment docs.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment on lines +127 to +129
pub fn new<L: Deref, E: EventHandler>(
payer: P, router: R, network_graph: G, logger: L, event_handler: E
) -> (Arc<Self>, PaymentRetryHandler<P, R, G, L, E>)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that it would be better to separate the construction of InvoicePayer and PaymentRetryHandler, as it wouldn't require using an Arc while still allowing it.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 910814f to ae0caddCompareAugust 24, 2021 17:15

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, simple, clean API. I like it.

Comment threadlightning-invoice/src/utils.rs
Comment threadlightning-invoice/src/payment.rs Outdated
}

/// An [`EventHandler`] decorator for retrying failed payments.
pub struct PaymentRetryHandler<I, P: Deref, R, G, L: Deref, E>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand why this is separate from the Payer - can't we/shouldn't we just implement EventHandler on the Payer?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

As of now, the ownership model is such that any decorating EventHandler (or BackgroundProcessor for that matter) would take ownership of this, which means InvoicePayer could then only be used as an event handler and no longer for initiating payments. For instance, we'll need to decorate this with NetworkUpdateHandler so that updates are applied before retries.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this). But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

Err, yes.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this)

Yes, along this line is what I was suggesting.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise. Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise.

I mean my usual reply applies - do users ever have a reasonable use case where they'd want to ignore most events?

Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

I'm not entirely convinced that it's clear in the current api that you have to hook these things up in a given order, whether it says retry or not. I wonder if we should make it more clear? Maybe have this object pass the update to the network graph itself?

}
}
/// Logs a byte slice in hex format.
#[macro_export]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually #[doc(hidden)] this and all the other macros in this file that really shoulnd't be used by downstream crates but only LDK crates?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see log_internal needing that. But which other ones? Presumably users may want to log within their trait implementations, possibly using log_bytes, too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I was mostly figuring the logging stuff is a one-way street - the lightning crates log stuff, and users consume it and put it somewhere. I suppose there's no real reason to require that, though, so, ok, fair enough. Please do add it to the internal one, though.

Comment threadlightning-invoice/src/payment.rs Outdated
fn pay_cached_invoice(&self, payment_hash: &PaymentHash) -> Result<(), PaymentError> {
let invoice_cache = self.invoice_cache.lock().unwrap();
match invoice_cache.get(payment_hash) {
Some(invoice) => self.pay_invoice_internal(invoice),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we somehow check that the route used on a retry is different from the original? I could see users using this API and just hooking it up to a remote route-getter which doesn't care about our failed payments.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that for simplicity this could be route-agnostic. Any changes to the network or scoring could affect the route calculation, so we should let those modules (e.g., higher-level event handlers) take care of making adjustments for the next time a route is fetched, if necessary, even if it means the same route is tried.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away. Maybe we should expose the retry count instead to make it explicit for users?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away.

Well, three times... first attempt counts as one. :)

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Maybe we should expose the retry count instead to make it explicit for users?

On the other hand, that would be a good argument for merging InvoicePayer and PaymentRetryHandle given the number of attempts is stored on the latter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Hmm, true. I guess you wouldn't generally use the invoice utilities in this case? I wonder if we merge the two and make the NetworkGraph an optional part if we can then just do retries if we have one?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... may work, though now we start to affect the Router interface as it still needs a NetworkGraph for the first attempt. We could make it an Option in find_route. Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.

If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.
If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

I think I'm a fan of this, yea, it reduces extra wires to hook up, better captures the set of likely user use-cases and Does The Right Thing, or at least makes the right thing more obvious, in common cases.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 3061394 to 97bd88cCompareAugust 26, 2021 22:26
@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Sep 10, 2021
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 52da654 to aa6b426CompareSeptember 15, 2021 17:38
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase now 🎉.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 4c64c33 to 9c09290CompareSeptember 15, 2021 21:52

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, but I like this API. Note that I don't think this can go in a release without #1053, so should either land after that, or at least in the same release.

Comment threadlightning-invoice/src/payment.rs Outdated

// Either the payment was rejected, exceeded the maximum attempts, or failed retry.
self.remove_cached_invoice(payment_hash);
attempts_by_payment_hash.remove(payment_hash);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it seems somewhat duplicative to get the entry above and then remove it here. You could just store the entry and remove the entry here, doing the and_modify.or_insert in the retry condition before the return.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the number of attempts using and_modify and or_insert for the comparison, which consumes the entry. But if no retry is needed we, couldn't then remove the entry directly since it has been consumed.

Probably could just do and_modify then match to determine whether or not there's a value, but that doesn't seem as clean.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not quite as clean, but it does feel incredibly wasteful to hash into the hashmap twice for the same element 5 lines apart :/

--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -202,11 +202,13 @@ where
let mut attempts_by_payment_hash = self.payment_attempts.lock().unwrap();
- let attempts = attempts_by_payment_hash
- .entry(*payment_hash)
- .and_modify(|attempts| *attempts += 1)
- .or_insert(1);
+ let attempts_entry = attempts_by_payment_hash
+ .entry(*payment_hash);
+ let attempts = if let hash_map::Entry::Occupied(e) = &attempts_entry { e.get() + 1 } else { 1 };
if !rejected_by_dest {
let max_payment_attempts = self.retry_attempts + 1;
- if *attempts < max_payment_attempts {
+ if attempts < max_payment_attempts {
if self.pay_cached_invoice(payment_hash).is_ok() {
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
+ attempts_entry
+ .and_modify(|attempts| *attempts += 1)
+ .or_insert(1);
return;
@@ -224,3 +226,2 @@ where
self.remove_cached_invoice(payment_hash);
- attempts_by_payment_hash.remove(payment_hash);
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you still need to remove the entry later, but that does work. However, there's still an additional look-up when remove_cached_invoice is called. Given no tests failed when the attempts entry was not removed -- and that when called independently remove_cached_invoice won't remove the attempts entry -- it probably makes sense to combine these two hash maps to ensure consistency. Along with your improvement, that takes us to one look-up instead of three.

Comment threadlightning-background-processor/src/lib.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from a308452 to 41d2b07CompareSeptember 20, 2021 19:47
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c6e3c06 to 4caedcfCompareSeptember 21, 2021 04:57
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Once #1084 is merged, I can rebase and fix the failing checks.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can be rebased now :)

@jkczyzjkczyz mentioned this pull request Sep 27, 2021
4 tasks
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 4caedcf to 35ad581CompareSeptember 27, 2021 21:35
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Note we should consider the invoice expiry when/before retrying.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c7d2086 to 14f7d04CompareSeptember 30, 2021 21:14
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
return;
} else {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the log could be a bit confusing here, because instead of a payment failed error, you immediately get an error retrying payment error.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let me know if you have a suggestion for rewording.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 3 times, most recently from 2aca5b1 to e7c1481CompareOctober 26, 2021 14:06
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Right, feels like we'll need to rewrite a good chunk of InvoicePayer to be stateless, might as well do it here instead of landing a bunch of code then replacing it tomorrow.

Ok, this is now using the parameters from PaymentPathFailed, so the only state is payment_cache for retry attempts. It's currently keyed by payment_hash, but I could change this to payment_id. I don't recall if we had a reason to prefer one over the other.

Otherwise, the main changes are:

  • Unified the get_route interface into a single find_route function so that Router only needs one method
  • Stored invoice expiry data in Payee since the Invoice is no longer cached
  • Removed commit adding a proof-of-concept test to BackgroundProcessor

Comment threadlightning/src/util/ser.rs Outdated
}
}

impl Writeable for Duration {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can we just write out a u64 instead? It feels a bit funky serializing a duration and losing a bunch of precision as a general thing?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed as per other discussion. Could also keep Duration and serialized as u128 nanoseconds. That said, for our use case, BOLT 11 defines the expiry time in seconds, so no need for more precision.

Comment threadlightning/src/routing/router.rs Outdated
/// Hints for routing to the payee, containing channels connecting the payee to public nodes.
pub route_hints: Vec<RouteHint>,

/// Expiration of a payment to the payee, relative to a user-defined epoch.

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a "user-defined epoch"? How is this expected to work with InvoicePayer - doesn't it always check against 1970? Can we just make this seconds-since-1970 and a u64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thought was to let the user set this relative to whatever they wanted because ultimately they would be setting it and interpreting it. So even if we say it's seconds since the UNIX epoch, there's really nothing enforcing that.

Anyhow, updated to u64 as discussed offline.

Comment threadlightning-invoice/src/payment.rs Outdated
// error occurred when attempting to retry.
entry.remove();
} else {
log_trace!(self.logger, "Unknown payment {}; cannot retry)", log_bytes!(payment_hash.0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to auto-insert here (and maybe a test for retry on reload, but I'm happy to add that as a followup for you).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to handle this and added a unit test.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from e7c1481 to c241093CompareOctober 26, 2021 17:35
/// The id returned by [`ChannelManager::send_payment`] and used with
/// [`ChannelManager::retry_payment`].
///
/// [`ChannelManager::send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just noticed that documentation for payment_id seems to be missing from ChannelMannager::send_payment. It would be nice to (a) document that the id should be stored for a retry, and (b) that it's needed because technically PaymentHashs can be non-unique.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving unresolved pending discussions around changes to send_payment.

write_tlv_fields!(writer, {
(0, payment_preimage, required),
(1, payment_hash, required),
(3, payment_id, option),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be a little nicer public API-wise if payment_id were a non-Option, and set to 0 if serialized before 0.0.104 (or wtv). Similar for the next commit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we leave it an Option for now and then just do a hard-break on our serialization backwards compat in like 2-3 versions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me! 🙂

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, maybe that is why we decided to key by payment_hash for now? I guess we could just not retry if PaymentId is None, though.

Comment threadlightning-invoice/src/payment.rs Outdated
}

/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {

@valentinewallacevalentinewallaceOct 26, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I'd feel better about this if we had a plan for this, it does seem like a lot of rewrites would have to happen to add spontaneous support (and feels odd to merge code that'll be definitely rewritten)? From Slack chats, it looks like the relevant user has implemented their own retries for now

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to properly interpret PaymentSendFailure errors but otherwise looking good, I think.

Comment threadfuzz/src/full_stack.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
let payment_secret = Some(invoice.payment_secret().clone());
let payment_id = self.payer.send_payment(&route, payment_hash, &payment_secret)
.map_err(|e| PaymentError::Sending(e))?;

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to figure out what's up - if we get a PaymentSendFailure::ParameterError or PaymentSendFailure::PathParameterError then we can return an error here, I think. AllFailedRetrySafe we should retry the requisite numbers right here, PartialFailure we should insert and retry the relevant paths.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. This may require a bit more work since we lose the PaymentId if send_payment returns an error. Ideally, we'd get back the proper RouteParameters for retry.

} else if has_expired(retry.as_ref().unwrap()) {
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else if self.retry_payment(*payment_id.as_ref().unwrap(), retry.as_ref().unwrap()).is_err() {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not always imply failure to send at all - see PaymentSendFailure and the notes at PaymentSendFailure::PartialFailure about when retry is safe.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these cases (both returned by send_payment and retry_payment), I take it we must retry here as there won't be a PaymentPathFailed event. Though one idea would be to generate them. It would make the retry attempt logic cleaner at very least.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK with #1141. Happy to land this as-is, but #1141 will have to be included in the same release.

An upcoming Router interface will be used for finding a Route both when
initially sending a payment and also when retrying failed payment paths.
Unify the three varieties of get_route so the interface can consist of a
single method implemented by the new `find_route` method. Give get_route
pub(crate) visibility so it can still be used in tests.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2864f61 to 2042241CompareOctober 27, 2021 01:16
When a payment fails, it's useful to retry the payment once the network
graph and channel scores are updated. InvoicePayer is a utility for
making payments which will retry any failed payment paths for a payment
up to a configured number of total attempts. It is parameterized by a
Payer and Router for ease of customization and testing.
Implement EventHandler for InvoicePayer as a decorator that intercepts
PaymentPathFailed events and retries that payment using the parameters
from the event. It delegates to the decorated EventHandler after retries
have been exhausted and for other events.
According to BOLT 11:
- after the `timestamp` plus `expiry` has passed
- SHOULD NOT attempt a payment
Add a convenience method for checking if an Invoice has expired, and use
it to short-circuit payment retries.
Implements Payer for ChannelManager and Rotuer for find_route, which can
be used to parameterize InvoicePayer when needing payment retries.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2042241 to 010436dCompareOctober 27, 2021 15:59
@jkczyzjkczyz mentioned this pull request Oct 27, 2021
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Diff from previous ACKs is trivial test changes, taking:

$ git diff-tree -U3 2042241f8885c0a17740b3867db4364be9fff29d 010436d07
diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs
index f51a4e580..7e931d66f 100644
--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -921,9 +921,9 @@ mod tests {
fn fails_on_attempt(self, attempt: usize) -> Self {
Self {
+ expectations: core::cell::RefCell::new(self.expectations.borrow().clone()),
attempts: core::cell::RefCell::new(0),
failing_on_attempt: Some(attempt),
- ..self
}
}
@@ -946,6 +946,18 @@ mod tests {
}
}
+	impl Drop for TestPayer {
+ fn drop(&mut self) {
+ if std::thread::panicking() {
+ return;
+ }
+
+ if !self.expectations.borrow().is_empty() {
+ panic!("Unsatisfied payment expectations: {:?}", self.expectations.borrow());
+ }
+ }
+	}
+
impl Payer for TestPayer {
fn node_id(&self) -> PublicKey {
let secp_ctx = Secp256k1::new();
$

@TheBlueMatt
TheBlueMatt merged commit 59659d3 into lightningdevkit:mainOct 27, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jkczyz@TheBlueMatt@arik-so@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Payment Retries by jkczyz · Pull Request #1059 · lightningdevkit/rust-lightning · GitHub
Skip to content

Payment Retries - #1059

Merged
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry
Oct 27, 2021
Merged

Payment Retries#1059
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry

Conversation

@jkczyz

@jkczyzjkczyz commented Aug 24, 2021

Copy link
Copy Markdown
Contributor

When a payment fails, it's useful to retry the payment once the network graph and channel scores are updated. InvoicePayer is a utility for making payments which caches the payment Invoice so that it can be used when retrying the payment. It is parameterized by a Payer and Router for ease of customization and testing.

PaymentRetryHandler is used in connection with InvoicePayer. It is an EventHandler decorator that intercepts PaymentFailed events and retries that payment using the cached Invoice.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from ec4d0c2 to 910814fCompareAugust 24, 2021 14:53
@codecov

codecovBot commented Aug 24, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1059 (0386410) into main (2ed1ba6) will increase coverage by 0.18%.
The diff coverage is 91.54%.

❗ Current head 0386410 differs from pull request most recent head 010436d. Consider uploading reports for the commit 010436d to get more accurate results
Impacted file tree graph

@@ Coverage Diff @@## main #1059 +/- ##
==========================================
+ Coverage 90.47% 90.66% +0.18% 
==========================================
Files 68 67 -1 Lines 35273 35261 -12 ==========================================
+ Hits 31914 31969 +55 + Misses 3359 3292 -67 
Impacted FilesCoverage Δ
lightning/src/util/events.rs31.98% <12.50%> (-1.36%)⬇️
lightning-invoice/src/utils.rs72.64% <16.66%> (-11.45%)⬇️
lightning-invoice/src/payment.rs95.11% <95.11%> (ø)
lightning-invoice/src/lib.rs88.59% <96.15%> (+0.48%)⬆️
lightning-background-processor/src/lib.rs94.52% <100.00%> (+0.28%)⬆️
lightning/src/ln/channelmanager.rs84.87% <100.00%> (+0.91%)⬆️
lightning/src/ln/functional_tests.rs97.44% <100.00%> (+0.07%)⬆️
lightning/src/ln/onion_route_tests.rs96.64% <100.00%> (+0.04%)⬆️
lightning/src/routing/network_graph.rs91.29% <100.00%> (ø)
lightning/src/util/logger.rs83.14% <100.00%> (+1.00%)⬆️
... and 32 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ed1ba6...010436d. Read the comment docs.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment on lines +127 to +129
pub fn new<L: Deref, E: EventHandler>(
payer: P, router: R, network_graph: G, logger: L, event_handler: E
) -> (Arc<Self>, PaymentRetryHandler<P, R, G, L, E>)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that it would be better to separate the construction of InvoicePayer and PaymentRetryHandler, as it wouldn't require using an Arc while still allowing it.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 910814f to ae0caddCompareAugust 24, 2021 17:15

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, simple, clean API. I like it.

Comment threadlightning-invoice/src/utils.rs
Comment threadlightning-invoice/src/payment.rs Outdated
}

/// An [`EventHandler`] decorator for retrying failed payments.
pub struct PaymentRetryHandler<I, P: Deref, R, G, L: Deref, E>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand why this is separate from the Payer - can't we/shouldn't we just implement EventHandler on the Payer?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

As of now, the ownership model is such that any decorating EventHandler (or BackgroundProcessor for that matter) would take ownership of this, which means InvoicePayer could then only be used as an event handler and no longer for initiating payments. For instance, we'll need to decorate this with NetworkUpdateHandler so that updates are applied before retries.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this). But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

Err, yes.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this)

Yes, along this line is what I was suggesting.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise. Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise.

I mean my usual reply applies - do users ever have a reasonable use case where they'd want to ignore most events?

Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

I'm not entirely convinced that it's clear in the current api that you have to hook these things up in a given order, whether it says retry or not. I wonder if we should make it more clear? Maybe have this object pass the update to the network graph itself?

}
}
/// Logs a byte slice in hex format.
#[macro_export]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually #[doc(hidden)] this and all the other macros in this file that really shoulnd't be used by downstream crates but only LDK crates?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see log_internal needing that. But which other ones? Presumably users may want to log within their trait implementations, possibly using log_bytes, too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I was mostly figuring the logging stuff is a one-way street - the lightning crates log stuff, and users consume it and put it somewhere. I suppose there's no real reason to require that, though, so, ok, fair enough. Please do add it to the internal one, though.

Comment threadlightning-invoice/src/payment.rs Outdated
fn pay_cached_invoice(&self, payment_hash: &PaymentHash) -> Result<(), PaymentError> {
let invoice_cache = self.invoice_cache.lock().unwrap();
match invoice_cache.get(payment_hash) {
Some(invoice) => self.pay_invoice_internal(invoice),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we somehow check that the route used on a retry is different from the original? I could see users using this API and just hooking it up to a remote route-getter which doesn't care about our failed payments.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that for simplicity this could be route-agnostic. Any changes to the network or scoring could affect the route calculation, so we should let those modules (e.g., higher-level event handlers) take care of making adjustments for the next time a route is fetched, if necessary, even if it means the same route is tried.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away. Maybe we should expose the retry count instead to make it explicit for users?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away.

Well, three times... first attempt counts as one. :)

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Maybe we should expose the retry count instead to make it explicit for users?

On the other hand, that would be a good argument for merging InvoicePayer and PaymentRetryHandle given the number of attempts is stored on the latter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Hmm, true. I guess you wouldn't generally use the invoice utilities in this case? I wonder if we merge the two and make the NetworkGraph an optional part if we can then just do retries if we have one?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... may work, though now we start to affect the Router interface as it still needs a NetworkGraph for the first attempt. We could make it an Option in find_route. Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.

If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.
If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

I think I'm a fan of this, yea, it reduces extra wires to hook up, better captures the set of likely user use-cases and Does The Right Thing, or at least makes the right thing more obvious, in common cases.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 3061394 to 97bd88cCompareAugust 26, 2021 22:26
@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Sep 10, 2021
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 52da654 to aa6b426CompareSeptember 15, 2021 17:38
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase now 🎉.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 4c64c33 to 9c09290CompareSeptember 15, 2021 21:52

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, but I like this API. Note that I don't think this can go in a release without #1053, so should either land after that, or at least in the same release.

Comment threadlightning-invoice/src/payment.rs Outdated

// Either the payment was rejected, exceeded the maximum attempts, or failed retry.
self.remove_cached_invoice(payment_hash);
attempts_by_payment_hash.remove(payment_hash);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it seems somewhat duplicative to get the entry above and then remove it here. You could just store the entry and remove the entry here, doing the and_modify.or_insert in the retry condition before the return.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the number of attempts using and_modify and or_insert for the comparison, which consumes the entry. But if no retry is needed we, couldn't then remove the entry directly since it has been consumed.

Probably could just do and_modify then match to determine whether or not there's a value, but that doesn't seem as clean.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not quite as clean, but it does feel incredibly wasteful to hash into the hashmap twice for the same element 5 lines apart :/

--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -202,11 +202,13 @@ where
let mut attempts_by_payment_hash = self.payment_attempts.lock().unwrap();
- let attempts = attempts_by_payment_hash
- .entry(*payment_hash)
- .and_modify(|attempts| *attempts += 1)
- .or_insert(1);
+ let attempts_entry = attempts_by_payment_hash
+ .entry(*payment_hash);
+ let attempts = if let hash_map::Entry::Occupied(e) = &attempts_entry { e.get() + 1 } else { 1 };
if !rejected_by_dest {
let max_payment_attempts = self.retry_attempts + 1;
- if *attempts < max_payment_attempts {
+ if attempts < max_payment_attempts {
if self.pay_cached_invoice(payment_hash).is_ok() {
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
+ attempts_entry
+ .and_modify(|attempts| *attempts += 1)
+ .or_insert(1);
return;
@@ -224,3 +226,2 @@ where
self.remove_cached_invoice(payment_hash);
- attempts_by_payment_hash.remove(payment_hash);
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you still need to remove the entry later, but that does work. However, there's still an additional look-up when remove_cached_invoice is called. Given no tests failed when the attempts entry was not removed -- and that when called independently remove_cached_invoice won't remove the attempts entry -- it probably makes sense to combine these two hash maps to ensure consistency. Along with your improvement, that takes us to one look-up instead of three.

Comment threadlightning-background-processor/src/lib.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from a308452 to 41d2b07CompareSeptember 20, 2021 19:47
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c6e3c06 to 4caedcfCompareSeptember 21, 2021 04:57
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Once #1084 is merged, I can rebase and fix the failing checks.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can be rebased now :)

@jkczyzjkczyz mentioned this pull request Sep 27, 2021
4 tasks
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 4caedcf to 35ad581CompareSeptember 27, 2021 21:35
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Note we should consider the invoice expiry when/before retrying.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c7d2086 to 14f7d04CompareSeptember 30, 2021 21:14
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
return;
} else {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the log could be a bit confusing here, because instead of a payment failed error, you immediately get an error retrying payment error.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let me know if you have a suggestion for rewording.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 3 times, most recently from 2aca5b1 to e7c1481CompareOctober 26, 2021 14:06
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Right, feels like we'll need to rewrite a good chunk of InvoicePayer to be stateless, might as well do it here instead of landing a bunch of code then replacing it tomorrow.

Ok, this is now using the parameters from PaymentPathFailed, so the only state is payment_cache for retry attempts. It's currently keyed by payment_hash, but I could change this to payment_id. I don't recall if we had a reason to prefer one over the other.

Otherwise, the main changes are:

  • Unified the get_route interface into a single find_route function so that Router only needs one method
  • Stored invoice expiry data in Payee since the Invoice is no longer cached
  • Removed commit adding a proof-of-concept test to BackgroundProcessor

Comment threadlightning/src/util/ser.rs Outdated
}
}

impl Writeable for Duration {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can we just write out a u64 instead? It feels a bit funky serializing a duration and losing a bunch of precision as a general thing?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed as per other discussion. Could also keep Duration and serialized as u128 nanoseconds. That said, for our use case, BOLT 11 defines the expiry time in seconds, so no need for more precision.

Comment threadlightning/src/routing/router.rs Outdated
/// Hints for routing to the payee, containing channels connecting the payee to public nodes.
pub route_hints: Vec<RouteHint>,

/// Expiration of a payment to the payee, relative to a user-defined epoch.

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a "user-defined epoch"? How is this expected to work with InvoicePayer - doesn't it always check against 1970? Can we just make this seconds-since-1970 and a u64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thought was to let the user set this relative to whatever they wanted because ultimately they would be setting it and interpreting it. So even if we say it's seconds since the UNIX epoch, there's really nothing enforcing that.

Anyhow, updated to u64 as discussed offline.

Comment threadlightning-invoice/src/payment.rs Outdated
// error occurred when attempting to retry.
entry.remove();
} else {
log_trace!(self.logger, "Unknown payment {}; cannot retry)", log_bytes!(payment_hash.0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to auto-insert here (and maybe a test for retry on reload, but I'm happy to add that as a followup for you).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to handle this and added a unit test.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from e7c1481 to c241093CompareOctober 26, 2021 17:35
/// The id returned by [`ChannelManager::send_payment`] and used with
/// [`ChannelManager::retry_payment`].
///
/// [`ChannelManager::send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just noticed that documentation for payment_id seems to be missing from ChannelMannager::send_payment. It would be nice to (a) document that the id should be stored for a retry, and (b) that it's needed because technically PaymentHashs can be non-unique.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving unresolved pending discussions around changes to send_payment.

write_tlv_fields!(writer, {
(0, payment_preimage, required),
(1, payment_hash, required),
(3, payment_id, option),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be a little nicer public API-wise if payment_id were a non-Option, and set to 0 if serialized before 0.0.104 (or wtv). Similar for the next commit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we leave it an Option for now and then just do a hard-break on our serialization backwards compat in like 2-3 versions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me! 🙂

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, maybe that is why we decided to key by payment_hash for now? I guess we could just not retry if PaymentId is None, though.

Comment threadlightning-invoice/src/payment.rs Outdated
}

/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {

@valentinewallacevalentinewallaceOct 26, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I'd feel better about this if we had a plan for this, it does seem like a lot of rewrites would have to happen to add spontaneous support (and feels odd to merge code that'll be definitely rewritten)? From Slack chats, it looks like the relevant user has implemented their own retries for now

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to properly interpret PaymentSendFailure errors but otherwise looking good, I think.

Comment threadfuzz/src/full_stack.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
let payment_secret = Some(invoice.payment_secret().clone());
let payment_id = self.payer.send_payment(&route, payment_hash, &payment_secret)
.map_err(|e| PaymentError::Sending(e))?;

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to figure out what's up - if we get a PaymentSendFailure::ParameterError or PaymentSendFailure::PathParameterError then we can return an error here, I think. AllFailedRetrySafe we should retry the requisite numbers right here, PartialFailure we should insert and retry the relevant paths.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. This may require a bit more work since we lose the PaymentId if send_payment returns an error. Ideally, we'd get back the proper RouteParameters for retry.

} else if has_expired(retry.as_ref().unwrap()) {
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else if self.retry_payment(*payment_id.as_ref().unwrap(), retry.as_ref().unwrap()).is_err() {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not always imply failure to send at all - see PaymentSendFailure and the notes at PaymentSendFailure::PartialFailure about when retry is safe.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these cases (both returned by send_payment and retry_payment), I take it we must retry here as there won't be a PaymentPathFailed event. Though one idea would be to generate them. It would make the retry attempt logic cleaner at very least.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK with #1141. Happy to land this as-is, but #1141 will have to be included in the same release.

An upcoming Router interface will be used for finding a Route both when
initially sending a payment and also when retrying failed payment paths.
Unify the three varieties of get_route so the interface can consist of a
single method implemented by the new `find_route` method. Give get_route
pub(crate) visibility so it can still be used in tests.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2864f61 to 2042241CompareOctober 27, 2021 01:16
When a payment fails, it's useful to retry the payment once the network
graph and channel scores are updated. InvoicePayer is a utility for
making payments which will retry any failed payment paths for a payment
up to a configured number of total attempts. It is parameterized by a
Payer and Router for ease of customization and testing.
Implement EventHandler for InvoicePayer as a decorator that intercepts
PaymentPathFailed events and retries that payment using the parameters
from the event. It delegates to the decorated EventHandler after retries
have been exhausted and for other events.
According to BOLT 11:
- after the `timestamp` plus `expiry` has passed
- SHOULD NOT attempt a payment
Add a convenience method for checking if an Invoice has expired, and use
it to short-circuit payment retries.
Implements Payer for ChannelManager and Rotuer for find_route, which can
be used to parameterize InvoicePayer when needing payment retries.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2042241 to 010436dCompareOctober 27, 2021 15:59
@jkczyzjkczyz mentioned this pull request Oct 27, 2021
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Diff from previous ACKs is trivial test changes, taking:

$ git diff-tree -U3 2042241f8885c0a17740b3867db4364be9fff29d 010436d07
diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs
index f51a4e580..7e931d66f 100644
--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -921,9 +921,9 @@ mod tests {
fn fails_on_attempt(self, attempt: usize) -> Self {
Self {
+ expectations: core::cell::RefCell::new(self.expectations.borrow().clone()),
attempts: core::cell::RefCell::new(0),
failing_on_attempt: Some(attempt),
- ..self
}
}
@@ -946,6 +946,18 @@ mod tests {
}
}
+	impl Drop for TestPayer {
+ fn drop(&mut self) {
+ if std::thread::panicking() {
+ return;
+ }
+
+ if !self.expectations.borrow().is_empty() {
+ panic!("Unsatisfied payment expectations: {:?}", self.expectations.borrow());
+ }
+ }
+	}
+
impl Payer for TestPayer {
fn node_id(&self) -> PublicKey {
let secp_ctx = Secp256k1::new();
$

@TheBlueMatt
TheBlueMatt merged commit 59659d3 into lightningdevkit:mainOct 27, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jkczyz@TheBlueMatt@arik-so@valentinewallace
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Payment Retries by jkczyz · Pull Request #1059 · lightningdevkit/rust-lightning · GitHub
Skip to content

Payment Retries - #1059

Merged
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry
Oct 27, 2021
Merged

Payment Retries#1059
TheBlueMatt merged 9 commits into
lightningdevkit:mainfrom
jkczyz:2021-08-payment-retry

Conversation

@jkczyz

@jkczyzjkczyz commented Aug 24, 2021

Copy link
Copy Markdown
Contributor

When a payment fails, it's useful to retry the payment once the network graph and channel scores are updated. InvoicePayer is a utility for making payments which caches the payment Invoice so that it can be used when retrying the payment. It is parameterized by a Payer and Router for ease of customization and testing.

PaymentRetryHandler is used in connection with InvoicePayer. It is an EventHandler decorator that intercepts PaymentFailed events and retries that payment using the cached Invoice.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from ec4d0c2 to 910814fCompareAugust 24, 2021 14:53
@codecov

codecovBot commented Aug 24, 2021

Copy link
Copy Markdown

Codecov Report

Merging #1059 (0386410) into main (2ed1ba6) will increase coverage by 0.18%.
The diff coverage is 91.54%.

❗ Current head 0386410 differs from pull request most recent head 010436d. Consider uploading reports for the commit 010436d to get more accurate results
Impacted file tree graph

@@ Coverage Diff @@## main #1059 +/- ##
==========================================
+ Coverage 90.47% 90.66% +0.18% 
==========================================
Files 68 67 -1 Lines 35273 35261 -12 ==========================================
+ Hits 31914 31969 +55 + Misses 3359 3292 -67 
Impacted FilesCoverage Δ
lightning/src/util/events.rs31.98% <12.50%> (-1.36%)⬇️
lightning-invoice/src/utils.rs72.64% <16.66%> (-11.45%)⬇️
lightning-invoice/src/payment.rs95.11% <95.11%> (ø)
lightning-invoice/src/lib.rs88.59% <96.15%> (+0.48%)⬆️
lightning-background-processor/src/lib.rs94.52% <100.00%> (+0.28%)⬆️
lightning/src/ln/channelmanager.rs84.87% <100.00%> (+0.91%)⬆️
lightning/src/ln/functional_tests.rs97.44% <100.00%> (+0.07%)⬆️
lightning/src/ln/onion_route_tests.rs96.64% <100.00%> (+0.04%)⬆️
lightning/src/routing/network_graph.rs91.29% <100.00%> (ø)
lightning/src/util/logger.rs83.14% <100.00%> (+1.00%)⬆️
... and 32 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ed1ba6...010436d. Read the comment docs.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment on lines +127 to +129
pub fn new<L: Deref, E: EventHandler>(
payer: P, router: R, network_graph: G, logger: L, event_handler: E
) -> (Arc<Self>, PaymentRetryHandler<P, R, G, L, E>)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that it would be better to separate the construction of InvoicePayer and PaymentRetryHandler, as it wouldn't require using an Arc while still allowing it.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 910814f to ae0caddCompareAugust 24, 2021 17:15

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, simple, clean API. I like it.

Comment threadlightning-invoice/src/utils.rs
Comment threadlightning-invoice/src/payment.rs Outdated
}

/// An [`EventHandler`] decorator for retrying failed payments.
pub struct PaymentRetryHandler<I, P: Deref, R, G, L: Deref, E>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand why this is separate from the Payer - can't we/shouldn't we just implement EventHandler on the Payer?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

As of now, the ownership model is such that any decorating EventHandler (or BackgroundProcessor for that matter) would take ownership of this, which means InvoicePayer could then only be used as an event handler and no longer for initiating payments. For instance, we'll need to decorate this with NetworkUpdateHandler so that updates are applied before retries.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this). But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from InvoicePayer, right? Payer is the trait parameter.

Err, yes.

I suppose we could implement EventHandler for T: Deref<Target = InvoicePayer<...>> and users could use an Arc<InvoicePayer<...>> (haven't actually tried this)

Yes, along this line is what I was suggesting.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it seems cleaner to separate paying from the retry behavior, which also lends to intuitive naming.

Hmm, I guess I don't see it, like, I have a thing that handles payment, I don't care about how it handles payment, or the fact that there's retries going on, I just want it to handle payment. Sure, I may need to wire it up, but having multiple things to wire up to get a payment to fire just seems like extra wires when its not adding flexibility for the user.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise. Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, though my remaining qualm is that you must pass it an EventHandler that way, whereas you have the option of not doing so otherwise.

I mean my usual reply applies - do users ever have a reasonable use case where they'd want to ignore most events?

Further, you do need to know that it is handling retries since that dictates how you compose different event handlers (e.g., decorating it with NetworkUpdateHandler and not the other way around). So maybe it's better to be explicit about it?

I'm not entirely convinced that it's clear in the current api that you have to hook these things up in a given order, whether it says retry or not. I wonder if we should make it more clear? Maybe have this object pass the update to the network graph itself?

}
}
/// Logs a byte slice in hex format.
#[macro_export]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually #[doc(hidden)] this and all the other macros in this file that really shoulnd't be used by downstream crates but only LDK crates?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see log_internal needing that. But which other ones? Presumably users may want to log within their trait implementations, possibly using log_bytes, too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I was mostly figuring the logging stuff is a one-way street - the lightning crates log stuff, and users consume it and put it somewhere. I suppose there's no real reason to require that, though, so, ok, fair enough. Please do add it to the internal one, though.

Comment threadlightning-invoice/src/payment.rs Outdated
fn pay_cached_invoice(&self, payment_hash: &PaymentHash) -> Result<(), PaymentError> {
let invoice_cache = self.invoice_cache.lock().unwrap();
match invoice_cache.get(payment_hash) {
Some(invoice) => self.pay_invoice_internal(invoice),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we somehow check that the route used on a retry is different from the original? I could see users using this API and just hooking it up to a remote route-getter which doesn't care about our failed payments.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that for simplicity this could be route-agnostic. Any changes to the network or scoring could affect the route calculation, so we should let those modules (e.g., higher-level event handlers) take care of making adjustments for the next time a route is fetched, if necessary, even if it means the same route is tried.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away. Maybe we should expose the retry count instead to make it explicit for users?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess it doesn't matter a ton, just feels like this may be a common thing for users to do, and we'd retry the same route four times and then fail 10 seconds later when we could have told the user that we failed right away.

Well, three times... first attempt counts as one. :)

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Maybe we should expose the retry count instead to make it explicit for users?

On the other hand, that would be a good argument for merging InvoicePayer and PaymentRetryHandle given the number of attempts is stored on the latter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about the remote route-getter use case. If it typically will return the same route, it seems retries would not be valuable, which is an argument for keeping InvoicePayer and PaymentRetryHandle separate.

Hmm, true. I guess you wouldn't generally use the invoice utilities in this case? I wonder if we merge the two and make the NetworkGraph an optional part if we can then just do retries if we have one?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... may work, though now we start to affect the Router interface as it still needs a NetworkGraph for the first attempt. We could make it an Option in find_route. Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.

If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could remove it entirely, so DefaultRouter would just hold its own NetworkGraph reference.
If we go with the latter option, then we could parameterize InvoicePayer with the number of attempts instead of using a constant, which would make the retry behavior explicit during its construction. This would of course require merging the two structs. How does that sound?

I think I'm a fan of this, yea, it reduces extra wires to hook up, better captures the set of likely user use-cases and Does The Right Thing, or at least makes the right thing more obvious, in common cases.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 3061394 to 97bd88cCompareAugust 26, 2021 22:26
@TheBlueMattTheBlueMatt added this to the 0.0.102 milestone Sep 10, 2021
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 52da654 to aa6b426CompareSeptember 15, 2021 17:38
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase now 🎉.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from 4c64c33 to 9c09290CompareSeptember 15, 2021 21:52

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, but I like this API. Note that I don't think this can go in a release without #1053, so should either land after that, or at least in the same release.

Comment threadlightning-invoice/src/payment.rs Outdated

// Either the payment was rejected, exceeded the maximum attempts, or failed retry.
self.remove_cached_invoice(payment_hash);
attempts_by_payment_hash.remove(payment_hash);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it seems somewhat duplicative to get the entry above and then remove it here. You could just store the entry and remove the entry here, doing the and_modify.or_insert in the retry condition before the return.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the number of attempts using and_modify and or_insert for the comparison, which consumes the entry. But if no retry is needed we, couldn't then remove the entry directly since it has been consumed.

Probably could just do and_modify then match to determine whether or not there's a value, but that doesn't seem as clean.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not quite as clean, but it does feel incredibly wasteful to hash into the hashmap twice for the same element 5 lines apart :/

--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -202,11 +202,13 @@ where
let mut attempts_by_payment_hash = self.payment_attempts.lock().unwrap();
- let attempts = attempts_by_payment_hash
- .entry(*payment_hash)
- .and_modify(|attempts| *attempts += 1)
- .or_insert(1);
+ let attempts_entry = attempts_by_payment_hash
+ .entry(*payment_hash);
+ let attempts = if let hash_map::Entry::Occupied(e) = &attempts_entry { e.get() + 1 } else { 1 };
if !rejected_by_dest {
let max_payment_attempts = self.retry_attempts + 1;
- if *attempts < max_payment_attempts {
+ if attempts < max_payment_attempts {
if self.pay_cached_invoice(payment_hash).is_ok() {
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
+ attempts_entry
+ .and_modify(|attempts| *attempts += 1)
+ .or_insert(1);
return;
@@ -224,3 +226,2 @@ where
self.remove_cached_invoice(payment_hash);
- attempts_by_payment_hash.remove(payment_hash);
},

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you still need to remove the entry later, but that does work. However, there's still an additional look-up when remove_cached_invoice is called. Given no tests failed when the attempts entry was not removed -- and that when called independently remove_cached_invoice won't remove the attempts entry -- it probably makes sense to combine these two hash maps to ensure consistency. Along with your improvement, that takes us to one look-up instead of three.

Comment threadlightning-background-processor/src/lib.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from a308452 to 41d2b07CompareSeptember 20, 2021 19:47
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c6e3c06 to 4caedcfCompareSeptember 21, 2021 04:57
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Once #1084 is merged, I can rebase and fix the failing checks.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Can be rebased now :)

@jkczyzjkczyz mentioned this pull request Sep 27, 2021
4 tasks
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 4caedcf to 35ad581CompareSeptember 27, 2021 21:35
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Note we should consider the invoice expiry when/before retrying.

Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 2 times, most recently from c7d2086 to 14f7d04CompareSeptember 30, 2021 21:14
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
return;
} else {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the log could be a bit confusing here, because instead of a payment failed error, you immediately get an error retrying payment error.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let me know if you have a suggestion for rewording.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch 3 times, most recently from 2aca5b1 to e7c1481CompareOctober 26, 2021 14:06
@jkczyz

Copy link
Copy Markdown
ContributorAuthor

Right, feels like we'll need to rewrite a good chunk of InvoicePayer to be stateless, might as well do it here instead of landing a bunch of code then replacing it tomorrow.

Ok, this is now using the parameters from PaymentPathFailed, so the only state is payment_cache for retry attempts. It's currently keyed by payment_hash, but I could change this to payment_id. I don't recall if we had a reason to prefer one over the other.

Otherwise, the main changes are:

  • Unified the get_route interface into a single find_route function so that Router only needs one method
  • Stored invoice expiry data in Payee since the Invoice is no longer cached
  • Removed commit adding a proof-of-concept test to BackgroundProcessor

Comment threadlightning/src/util/ser.rs Outdated
}
}

impl Writeable for Duration {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can we just write out a u64 instead? It feels a bit funky serializing a duration and losing a bunch of precision as a general thing?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed as per other discussion. Could also keep Duration and serialized as u128 nanoseconds. That said, for our use case, BOLT 11 defines the expiry time in seconds, so no need for more precision.

Comment threadlightning/src/routing/router.rs Outdated
/// Hints for routing to the payee, containing channels connecting the payee to public nodes.
pub route_hints: Vec<RouteHint>,

/// Expiration of a payment to the payee, relative to a user-defined epoch.

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a "user-defined epoch"? How is this expected to work with InvoicePayer - doesn't it always check against 1970? Can we just make this seconds-since-1970 and a u64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thought was to let the user set this relative to whatever they wanted because ultimately they would be setting it and interpreting it. So even if we say it's seconds since the UNIX epoch, there's really nothing enforcing that.

Anyhow, updated to u64 as discussed offline.

Comment threadlightning-invoice/src/payment.rs Outdated
// error occurred when attempting to retry.
entry.remove();
} else {
log_trace!(self.logger, "Unknown payment {}; cannot retry)", log_bytes!(payment_hash.0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to auto-insert here (and maybe a test for retry on reload, but I'm happy to add that as a followup for you).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to handle this and added a unit test.

@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from e7c1481 to c241093CompareOctober 26, 2021 17:35
/// The id returned by [`ChannelManager::send_payment`] and used with
/// [`ChannelManager::retry_payment`].
///
/// [`ChannelManager::send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just noticed that documentation for payment_id seems to be missing from ChannelMannager::send_payment. It would be nice to (a) document that the id should be stored for a retry, and (b) that it's needed because technically PaymentHashs can be non-unique.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving unresolved pending discussions around changes to send_payment.

write_tlv_fields!(writer, {
(0, payment_preimage, required),
(1, payment_hash, required),
(3, payment_id, option),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be a little nicer public API-wise if payment_id were a non-Option, and set to 0 if serialized before 0.0.104 (or wtv). Similar for the next commit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we leave it an Option for now and then just do a hard-break on our serialization backwards compat in like 2-3 versions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me! 🙂

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, maybe that is why we decided to key by payment_hash for now? I guess we could just not retry if PaymentId is None, though.

Comment threadlightning-invoice/src/payment.rs Outdated
}

/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {

@valentinewallacevalentinewallaceOct 26, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I'd feel better about this if we had a plan for this, it does seem like a lot of rewrites would have to happen to add spontaneous support (and feels odd to merge code that'll be definitely rewritten)? From Slack chats, it looks like the relevant user has implemented their own retries for now

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to properly interpret PaymentSendFailure errors but otherwise looking good, I think.

Comment threadfuzz/src/full_stack.rs Outdated
Comment threadlightning-invoice/src/payment.rs Outdated
Comment threadlightning-invoice/src/payment.rs
Comment threadlightning-invoice/src/payment.rs Outdated
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
let payment_secret = Some(invoice.payment_secret().clone());
let payment_id = self.payer.send_payment(&route, payment_hash, &payment_secret)
.map_err(|e| PaymentError::Sending(e))?;

@TheBlueMattTheBlueMattOct 26, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to figure out what's up - if we get a PaymentSendFailure::ParameterError or PaymentSendFailure::PathParameterError then we can return an error here, I think. AllFailedRetrySafe we should retry the requisite numbers right here, PartialFailure we should insert and retry the relevant paths.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. This may require a bit more work since we lose the PaymentId if send_payment returns an error. Ideally, we'd get back the proper RouteParameters for retry.

} else if has_expired(retry.as_ref().unwrap()) {
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else if self.retry_payment(*payment_id.as_ref().unwrap(), retry.as_ref().unwrap()).is_err() {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not always imply failure to send at all - see PaymentSendFailure and the notes at PaymentSendFailure::PartialFailure about when retry is safe.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these cases (both returned by send_payment and retry_payment), I take it we must retry here as there won't be a PaymentPathFailed event. Though one idea would be to generate them. It would make the retry attempt logic cleaner at very least.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK with #1141. Happy to land this as-is, but #1141 will have to be included in the same release.

An upcoming Router interface will be used for finding a Route both when
initially sending a payment and also when retrying failed payment paths.
Unify the three varieties of get_route so the interface can consist of a
single method implemented by the new `find_route` method. Give get_route
pub(crate) visibility so it can still be used in tests.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2864f61 to 2042241CompareOctober 27, 2021 01:16
When a payment fails, it's useful to retry the payment once the network
graph and channel scores are updated. InvoicePayer is a utility for
making payments which will retry any failed payment paths for a payment
up to a configured number of total attempts. It is parameterized by a
Payer and Router for ease of customization and testing.
Implement EventHandler for InvoicePayer as a decorator that intercepts
PaymentPathFailed events and retries that payment using the parameters
from the event. It delegates to the decorated EventHandler after retries
have been exhausted and for other events.
According to BOLT 11:
- after the `timestamp` plus `expiry` has passed
- SHOULD NOT attempt a payment
Add a convenience method for checking if an Invoice has expired, and use
it to short-circuit payment retries.
Implements Payer for ChannelManager and Rotuer for find_route, which can
be used to parameterize InvoicePayer when needing payment retries.
@jkczyz
jkczyzforce-pushed the 2021-08-payment-retry branch from 2042241 to 010436dCompareOctober 27, 2021 15:59
@jkczyzjkczyz mentioned this pull request Oct 27, 2021
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Diff from previous ACKs is trivial test changes, taking:

$ git diff-tree -U3 2042241f8885c0a17740b3867db4364be9fff29d 010436d07
diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs
index f51a4e580..7e931d66f 100644
--- a/lightning-invoice/src/payment.rs
+++ b/lightning-invoice/src/payment.rs
@@ -921,9 +921,9 @@ mod tests {
fn fails_on_attempt(self, attempt: usize) -> Self {
Self {
+ expectations: core::cell::RefCell::new(self.expectations.borrow().clone()),
attempts: core::cell::RefCell::new(0),
failing_on_attempt: Some(attempt),
- ..self
}
}
@@ -946,6 +946,18 @@ mod tests {
}
}
+	impl Drop for TestPayer {
+ fn drop(&mut self) {
+ if std::thread::panicking() {
+ return;
+ }
+
+ if !self.expectations.borrow().is_empty() {
+ panic!("Unsatisfied payment expectations: {:?}", self.expectations.borrow());
+ }
+ }
+	}
+
impl Payer for TestPayer {
fn node_id(&self) -> PublicKey {
let secp_ctx = Secp256k1::new();
$

@TheBlueMatt
TheBlueMatt merged commit 59659d3 into lightningdevkit:mainOct 27, 2021
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jkczyz@TheBlueMatt@arik-so@valentinewallace