-
Notifications
You must be signed in to change notification settings - Fork 421
Always-online node forward invoice request #4049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Always-online node forward invoice request #4049
Conversation
|
👋 I see @tankyleo was un-assigned. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4049 +/- ##
==========================================
- Coverage 88.76% 88.35% -0.41%
==========================================
Files 176 177 +1
Lines 129345 131816 +2471
Branches 129345 131816 +2471
==========================================
+ Hits 114812 116465 +1653
- Misses 11925 12692 +767
- Partials 2608 2659 +51
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
🔔 1st Reminder Hey @tankyleo! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @tankyleo! This PR has been waiting for your review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Took a first pass, just have a conceptual question so far, thank you for the tests.
lightning/src/ln/channelmanager.rs
Outdated
| /// When handling an [`Event::StaticInvoiceRequested`], this should be called to forward the | ||
| /// [`InvoiceRequest`] over the `invoice_request_path` to the async recipient if it is online | ||
| /// and it will forward the [`StaticInvoice`] to the responder. | ||
| pub fn send_response_static_invoice_request( | ||
| &self, invoice: StaticInvoice, responder: Responder, invoice_request: InvoiceRequest, | ||
| invoice_request_path: BlindedMessagePath, | ||
| ) -> Result<(), Bolt12SemanticError> { | ||
| self.flow.enqueue_invoice_request_to_forward( | ||
| invoice_request, | ||
| invoice_request_path, | ||
| responder.clone(), | ||
| ); | ||
| self.flow.enqueue_static_invoice(invoice, responder) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the goal here to allow an async-recipient to "override" the invoice that would have been provided by the server since the recipient is online ? If so, should the static invoice server add a small delay before serving the invoice back to the sender to give the async-recipient a chance to respond to the sender first ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the goal here to allow an async-recipient to "override" the invoice that would have been provided by the server since the recipient is online ?
yes!
should the static invoice server add a small delay before serving the invoice back to the sender to give the async-recipient a chance to respond to the sender first ?
yeah I thought about something like that to allow the receiver to respond. I got from @valentinewallace that for this first iteration to forward both of them immediately and perhaps in a future PR on the sender side detect if it received a fresh invoice from the async recipient and pay that one instead. Will defer to her :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you consider splitting the forwarding part and the response part into two methods on ChannelManager ? Like this the user could first call the "forward static invoice request" method, wait some chosen amount of time, then call "respond with persisted static invoice".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't consider separating like that. Thanks, I can try that 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda feel like a better UX is more important, which I feel like a single method would be? Also this makes forwarding the invreq optional, whereas before it was required. We could handle the wait in LDK in std builds, theoretically, but in practice I don't feel like we'll ever want to increase potential payment latency for any reason. The sender can also wait a second for a fresh invoice if they choose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good we can go with the single-method approach then @elnosh
|
👋 The first review has been submitted! Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer. |
|
@elnosh Is this good to take a look ? Feel free to hit the "request review" button top right. |
ah, yes. Added a fixup breaking up the forwarding of the request as suggested. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks better to me thank you ! A couple more comments
| impl PartialEq for InvoiceRequest { | ||
| fn eq(&self, other: &Self) -> bool { | ||
| self.bytes.eq(&other.bytes) | ||
| } | ||
| } | ||
|
|
||
| impl Eq for InvoiceRequest {} | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I would have kept the equality checks on all members for tests to make sure the deserialization from bytes works properly:
diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs
index 8956fc6ca..d5d3c4d75 100644
--- a/lightning/src/offers/invoice_request.rs
+++ b/lightning/src/offers/invoice_request.rs
@@ -581,15 +581,17 @@ impl AsRef<TaggedHash> for UnsignedInvoiceRequest {
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
/// [`Offer`]: crate::offers::offer::Offer
#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
pub struct InvoiceRequest {
pub(super) bytes: Vec<u8>,
pub(super) contents: InvoiceRequestContents,
signature: Signature,
}
+#[cfg(not(test))]
impl PartialEq for InvoiceRequest {
fn eq(&self, other: &Self) -> bool {
- self.bytes.eq(&other.bytes)
+ self.bytes.eq(&other.bytes) && self.signature.eq(&other.signature)
}
}
lightning/src/offers/flow.rs
Outdated
| /// The invoice request that should be forwarded to the async recipient in case it is | ||
| /// online to respond. | ||
| invoice_request: InvoiceRequest, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets point people to OffersMessageFlow::enqueue_invoice_request_to_forward here
| claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); | ||
| assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); | ||
|
|
||
| // After paying the static invoice, check that regular invoice received from async recipient is ignored. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For completeness, let's also cover the case where the async recipient responds first ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically looks solid, thanks for this @elnosh!
lightning/src/ln/channelmanager.rs
Outdated
| /// When handling an [`Event::StaticInvoiceRequested`], this should be called to forward the | ||
| /// [`InvoiceRequest`] over the `invoice_request_path` to the async recipient if it is online | ||
| /// and it will forward the [`StaticInvoice`] to the responder. | ||
| pub fn send_response_static_invoice_request( | ||
| &self, invoice: StaticInvoice, responder: Responder, invoice_request: InvoiceRequest, | ||
| invoice_request_path: BlindedMessagePath, | ||
| ) -> Result<(), Bolt12SemanticError> { | ||
| self.flow.enqueue_invoice_request_to_forward( | ||
| invoice_request, | ||
| invoice_request_path, | ||
| responder.clone(), | ||
| ); | ||
| self.flow.enqueue_static_invoice(invoice, responder) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda feel like a better UX is more important, which I feel like a single method would be? Also this makes forwarding the invreq optional, whereas before it was required. We could handle the wait in LDK in std builds, theoretically, but in practice I don't feel like we'll ever want to increase potential payment latency for any reason. The sender can also wait a second for a fresh invoice if they choose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your work on this @elnosh. It looks really good and helped my understanding of async receive.
| let invoice_flow_res = | ||
| pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); | ||
| let static_invoice = invoice_flow_res.invoice; | ||
| let static_invoice = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: is there a reason you separated the call to pass_static_invoice_server_messages() and extracting the static_invoice value elsewhere, but combined them here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah from latest changes where I reverted back to send_response_static_invoice_request I separated it to also use the invoice_request_path.
lightning/src/ln/channelmanager.rs
Outdated
| /// When handling an [`Event::StaticInvoiceRequested`], this should be called to forward the | ||
| /// [`InvoiceRequest`] over the `invoice_request_path` to the async recipient in case it is | ||
| /// online to respond. | ||
| pub fn forward_invoice_request( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noob question: I understand this is meant to be called/used when the async recipient is online, but what happens if they're not? I'd expect to see some kind of fallback mechanism that allows us to forward a static invoice, or is that something the user is expected to handle?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah if the recipient is not online it should fallback to paying static invoice it got from the always-online node. I reverted this method and using send_response_static_invoice_request where it does both. It forwards the invoice request to the async receiver and sends the static invoice to the payer. So if recipient is not online, payer should still get the static invoice and be able to pay that one.
be502cb to
f109e16
Compare
|
thank you for the reviews!
|
| #[cfg(not(test))] | ||
| impl PartialEq for InvoiceRequest { | ||
| fn eq(&self, other: &Self) -> bool { | ||
| self.bytes.eq(&other.bytes) && self.signature.eq(&other.signature) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PartialEq implementation is conditionally excluded from test builds with #[cfg(not(test))]. This will prevent comparing InvoiceRequest instances in tests, causing compilation failures when using equality assertions or comparisons. Consider either:
- Removing the conditional compilation to make
PartialEqavailable in all contexts - Providing an alternative
PartialEqimplementation for test builds
This ensures consistent behavior across both production and test environments.
| #[cfg(not(test))] | |
| impl PartialEq for InvoiceRequest { | |
| fn eq(&self, other: &Self) -> bool { | |
| self.bytes.eq(&other.bytes) && self.signature.eq(&other.signature) | |
| } | |
| } | |
| impl PartialEq for InvoiceRequest { | |
| fn eq(&self, other: &Self) -> bool { | |
| self.bytes.eq(&other.bytes) && self.signature.eq(&other.signature) | |
| } | |
| } | |
Spotted by Diamond
Is this helpful? React 👍 or 👎 to let us know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically LGTM, feel free to squash
| /// later provided to [`ChannelManager::send_response_static_invoice_request`]. | ||
| /// | ||
| /// [`ChannelManager::send_response_static_invoice_request`]: crate::ln::channelmanager::ChannelManager::send_response_static_invoice_request | ||
| invoice_request_path: BlindedMessagePath, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if it's awkward that we have a raw BlindedMessagePath here but use a Responder below... Not blocking though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm yeah can be a bit awkward. I used a BlindedMessagePath since that is how it's sent in the ServeStaticInvoice message. And if passed as a Responder I think I would still need to use it as a BlindedMessagePath to build the MessageSendInstructions::WithSpecifiedReplyPath
f109e16 to
1266567
Compare
Previously it would generate a new nonce for this context instead of using the offer nonce. This would make it so that verification would fail later when receiving a invoice request.
1266567 to
fa6f20d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, feel free to squash
As a static invoice server, if we receive an invoice request on behalf of an often-offline recipient we will reply to the sender with the static invoice previously provided by the async recipient. Here, in addition to doing that we'll forward the invoice request received to the async recipient to give it a chance to reply with a fresh invoice in case it is online.
fa6f20d to
f46a3ff
Compare
| #[cfg(not(test))] | ||
| impl PartialEq for InvoiceRequest { | ||
| fn eq(&self, other: &Self) -> bool { | ||
| self.bytes.eq(&other.bytes) && self.signature.eq(&other.signature) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think based on the PartialEq implementation for Bolt12Invoice, it would be sufficient to just check the bytes here. IIRC the signature is included in the bytes
|
@tankyleo I'm good to land this when you are |
|
🔔 1st Reminder Hey @tankyleo! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @tankyleo! This PR has been waiting for your review. |
| invoice_request_path, | ||
| responder.clone(), | ||
| ); | ||
| self.flow.enqueue_static_invoice(invoice, responder) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this work? If we immediately send the static invoice the sender will ~always see that first and use that, and the optimistic non-static invoice will never be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, @valentinewallace pointed out that this is really for the sender-doesn't-support-async case, not really for "upgrading" to non-static invoices when the recipient is online. We should write that somewhere...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it was an oversight not to document that. It was actually pointed out somewhat late in the game by @joostjager here: #4045 (comment)
For more context, I had also told @elnosh that in follow-up we should add support for an always-online sender swapping out a pending outbound to a static invoice for one with a fresh invoice, if one comes in. Doesn't cover all cases though ofc.
| /// The invoice request that will be forwarded to the async recipient to give the | ||
| /// recipient a chance to provide an invoice in case it is online. It should be | ||
| /// provided to [`ChannelManager::respond_to_static_invoice_request`]. | ||
| /// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably we should document here that LSPs should try to use LSPS5 to wake the client (based on the invoice_request_path first-hop, presumably? or is there a place where the LSP sees the counterparty node id? I don't see one in PersistStaticInvoice). From there the LSP should give them a second to come online before calling respond_to_static_invoice_request to make sure they have a shot at receiving the message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed it more with @valentinewallace really I think we need to be pushing the forwarded static invoice requests through the OnionMessenger forwarding pipeline (so we use the onion message mailbox logic and so that we avoid the direct-peer-connect logic) rather than the sending pipeline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LSPs should try to use LSPS5 to wake the client (based on the
invoice_request_pathfirst-hop, presumably?
I was wondering why not this approach instead of the mailbox logic? It could be weird to have this flow outside of the mailbox but as you said it would allow the LSP to wake up the client before deciding to call respond_to_static_invoice_request to send the static invoice back. Which with the mailbox I think it will do both at the same time - generate the OnionMessageIntercepted event and send back the static invoice unless we separate them.
Also, in any of the 2 approaches, although I would think most likely the introduction node of the invoice_request_path will be the LSP, there's no way to guarantee that. So we would also need to first check the first hop and if it's not the LSP, just send the message?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be in addition/conjunction with the mailbox logic. LSPS5 is just a way to tell the client to run by sending a notification, we'd still have to keep the pending message in the mailbox so that we can deliver it when they come online.
|
I tried to update ldk-node to this PR, but it failed the static invoice server test: lightningdevkit/ldk-node#635
The logs are written to stdout with a node id at the start of the line, but still it is quite hard to debug. So many onion messages and/or retries and/or different paths involved. I suspect it has something to do with the reply race introduced in this PR? Maybe it's not worth looking at, because with #4046 it seems fixed. It would be nice though to understand what's going on. Also building ldk-node against current ldk main isn't possible atm, so it is blocking other work. |
|
is it good now? seems fixed in #4078 |
Part of #2298
As an always online node if we receive a invoice request on behalf of an async recipient, add support for forwarding the request to the recipient in case it is online to respond to the request.