-
Notifications
You must be signed in to change notification settings - Fork 419
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
👋 Thanks for assigning @valentinewallace as a reviewer! |
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
PartialEq
available in all contexts - Providing an alternative
PartialEq
implementation 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, | ||
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_path
first-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?
With the merge of lightningdevkit/rust-lightning#4049, it is now possible for a static invoice server to forward the invoice request to the recipient if they are online.
With the merge of lightningdevkit/rust-lightning#4049, it is now possible for a static invoice server to forward the invoice request to the recipient if they are online.
With the merge of lightningdevkit/rust-lightning#4049, it is now possible for a static invoice server to forward the invoice request to the recipient if they are online.
With the merge of lightningdevkit/rust-lightning#4049, it is now possible for a static invoice server to forward the invoice request to the recipient if they are 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. |
With the merge of lightningdevkit/rust-lightning#4049, it is now possible for a static invoice server to forward the invoice request to the recipient if they are online.
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.