Skip to content
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

BOLT 12 Offers message handling support #2294

Merged
merged 15 commits into from
Jun 13, 2023

Conversation

jkczyz
Copy link
Contributor

@jkczyz jkczyz commented May 15, 2023

Add support for handling BOLT 12 Offers messages and, more generally, replying to onion messages.

  • Add InvoiceError message
  • Expand OnionMessageContents with Offers messages
  • Parameterize OnionMessenger with an OffersMessageHandler trait
  • Parameterize OnionMessenger with an MessageRouter trait for routing replies
  • Support replies in CustomOnionMessageHandler and OffersMessageHandler

A follow-up (#2371) implements OffersMessageHandler for ChannelManager.

@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch from da21b8d to 64007e7 Compare May 15, 2023 20:07
@codecov-commenter
Copy link

codecov-commenter commented May 15, 2023

Codecov Report

Patch coverage: 75.88% and project coverage change: +1.07 🎉

Comparison is base (d78dd48) 90.48% compared to head (3a316cc) 91.56%.

❗ Current head 3a316cc differs from pull request most recent head 3fd6b44. Consider uploading reports for the commit 3fd6b44 to get more accurate results

❗ Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please install the Github App Integration for your organization. Read more.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2294      +/-   ##
==========================================
+ Coverage   90.48%   91.56%   +1.07%     
==========================================
  Files         104      106       +2     
  Lines       53920    66738   +12818     
  Branches    53920    66738   +12818     
==========================================
+ Hits        48792    61106   +12314     
- Misses       5128     5632     +504     
Impacted Files Coverage Δ
lightning-background-processor/src/lib.rs 86.54% <ø> (+3.96%) ⬆️
lightning/src/onion_message/offers.rs 8.33% <8.33%> (ø)
lightning/src/ln/peer_handler.rs 66.42% <33.33%> (+4.60%) ⬆️
lightning/src/onion_message/packet.rs 76.92% <66.66%> (+1.39%) ⬆️
lightning/src/onion_message/messenger.rs 87.43% <76.31%> (-3.32%) ⬇️
lightning/src/offers/invoice_error.rs 82.64% <82.64%> (ø)
lightning/src/util/ser.rs 85.06% <83.33%> (-0.02%) ⬇️
lightning/src/onion_message/functional_tests.rs 92.88% <90.24%> (-1.78%) ⬇️
lightning/src/util/ser_macros.rs 69.06% <100.00%> (-1.74%) ⬇️

... and 27 files with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@dunxen dunxen self-requested a review May 16, 2023 10:23
Copy link
Contributor

@dunxen dunxen left a comment

Choose a reason for hiding this comment

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

Checked the TLV types against spec and have done a brief review while just brushing up some of the spec again.

Just a nit so far. Will do more review tomorrow.

Copy link
Contributor

@carlaKC carlaKC left a comment

Choose a reason for hiding this comment

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

New here - hopefully some of what I've said makes sense.

///
/// [`OnionMessage`]: msgs::OnionMessage
pub trait MessageRouter {
/// Returns a route for sending an [`OnionMessage`] to the given [`Destination`].
Copy link
Contributor

Choose a reason for hiding this comment

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

Make it clearer here that find_route should return only intermediate_hops to the destination?
I would expect a find_route call to also include the pubkey of the final node (or intro node), but when it's used in the next commit with send_custom_message it's expected to only return the intermediate hops.

Also, empty vector = no path?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Make it clearer here that find_route should return only intermediate_hops to the destination?

Add clarification to the docs.

I would expect a find_route call to also include the pubkey of the final node (or intro node), but when it's used in the next commit with send_custom_message it's expected to only return the intermediate hops.

Perhaps there's a better name than find_route or at very least a better name for what it is returning. Open to suggestions.

Also, empty vector = no path?

This would be for if there is a direct connection between sender and recipient.

Copy link
Contributor

Choose a reason for hiding this comment

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

find_hops?
Sounds like a task for a brewery though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could be get_intermediate_hops since that's the wording used in send_custom_message? But no strong feelings if the docs are updated.

Copy link
Contributor

@valentinewallace valentinewallace May 23, 2023

Choose a reason for hiding this comment

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

Another option could be to pass in a new onion_message::Path (or similar name) into send_onion_message, consolidating the intermediate_nodes and destination parameters. Then find_path could return a Path. This'd be in-line with our payment apis.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@TheBlueMatt Would you be ok with that or will this make bindings difficult?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed offline and at the review club. Renamed to find_path taking a Destination by value and returning a new OnionMessagePath struct. Also, updated OnionMessenger::send_custom_message to take this struct instead of the two parameters as suggested earlier.

Regarding naming, not sure if I should rename MessageRouter to OnionMessageRouter for consistency with the new struct and OnionMessageContents.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@TheBlueMatt Would you be ok with that or will this make bindings difficult?

Not sure why this would make the bindings difficult?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The concern was around having two items in different modules named Path.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, yea, we can't do that for bindings, and really shouldnt do it for rust users either.

@jkczyz jkczyz mentioned this pull request May 18, 2023
60 tasks
@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch from 64007e7 to 238b72e Compare May 19, 2023 15:57
Copy link
Contributor Author

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Thanks for the review!

///
/// [`OnionMessage`]: msgs::OnionMessage
pub trait MessageRouter {
/// Returns a route for sending an [`OnionMessage`] to the given [`Destination`].
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Make it clearer here that find_route should return only intermediate_hops to the destination?

Add clarification to the docs.

I would expect a find_route call to also include the pubkey of the final node (or intro node), but when it's used in the next commit with send_custom_message it's expected to only return the intermediate hops.

Perhaps there's a better name than find_route or at very least a better name for what it is returning. Open to suggestions.

Also, empty vector = no path?

This would be for if there is a direct connection between sender and recipient.

///
/// [`OnionMessage`]: msgs::OnionMessage
pub trait MessageRouter {
/// Returns a route for sending an [`OnionMessage`] to the given [`Destination`].
Copy link
Contributor

Choose a reason for hiding this comment

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

find_hops?
Sounds like a task for a brewery though.

@valentinewallace valentinewallace self-requested a review May 22, 2023 15:41
Copy link
Contributor

@valentinewallace valentinewallace left a comment

Choose a reason for hiding this comment

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

Seems like mostly trivial changes? Hope we can land this soon!


let erroneous_field = match (erroneous_field, suggested_value) {
(None, None) => None,
(None, Some(_)) => None,
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, though the spec is underspecified. I left a comment: https://github.com/lightning/bolts/pull/798/files#r1200840518

I wonder if we should just ignore erroneous_field and suggested_value since they are odd and not very useful at the moment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI, I added an additional error here. Discussion from the spec meeting indicated the error fields are meant to be generic enough for future uses. So might as well support reading them at least.

match Self::parse(tlv_type, bytes) {
Ok(message) => Ok(message),
Err(ParseError::Decode(e)) => Err(e),
Err(_) => Err(DecodeError::InvalidValue), // TODO: log ParseError?
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd be in favor of logging 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm... seems I'd have to update all the Readable / ReadableArgs impls back to wire::Message to take a logger to use here. Perhaps it would be better to make a new DecodeError variant wrapping the ParseError to avoid that? That was we could log in PeerManager. Let me know if you have a preference or another idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Went ahead and made a couple new DecodeError variants. PTAL

@@ -115,6 +122,12 @@ impl CustomOnionMessageHandler for TestCustomMessageHandler {
}
}

impl ResponseErrorHandler for TestCustomMessageHandler {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this additional trait, instead of returning a Result from handle_custom_message? We could also add an event for failed OM handling.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The error happens after calling handle_custom_message when OnionMessenger attempts to send the reply. This avoid reentrancy when if instead the user was given a reference to OnionMessenger to directly send the reply.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be reasonable to implement EventsProvider for Messenger and generate OM failure events there? Seems in line with what we do for outbound payment path failures

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possibly... @TheBlueMatt is there any concern here? I'd imagine we may want to avoid onion messages from an untrusted source filling our event queue at no cost. At very least the cost for an outbound payment is to tie up liquidity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed offline. For now we'll leave the trait but may want to have the ChannelManager implementation produce an event in the future.

Copy link
Contributor

@alecchendev alecchendev left a comment

Choose a reason for hiding this comment

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

Most of the stuff I was thinking to comment on while reviewing was already addressed by others so LGTM 👍

Comment on lines 471 to 497
OnionMessageContents::Offers(_) => self.respond_with_onion_message(
response, path_id, reply_path, &*self.offers_handler
),
OnionMessageContents::Custom(_) => self.respond_with_onion_message(
response, path_id, reply_path, &*self.custom_handler
),
Copy link
Contributor

Choose a reason for hiding this comment

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

Can someone explain the &* here? I've been running into this more lately and haven't quite understood it. Also I notice if I remove the Sized restriction on OMH::Target: OffersMessageHandler + Sized--which is self.offers_handler's type--this gets a warning and I also don't really understand why. I get basic de/referencing and that implementing the Sized trait means having a known constant size at compile time but don't understand it's use/need in this context.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can someone explain the &* here? I've been running into this more lately and haven't quite understood it.

self.custom_handler is a type implementing Deref where the Target implements CustomOnionMessageHandler. respond_with_onion_message wants a reference to a type implementing ResponseErrorHandler, which is a supertrait. It seems just calling deref() would have sufficed.

Also I notice if I remove the Sized restriction on OMH::Target: OffersMessageHandler + Sized--which is self.offers_handler's type--this gets a warning and I also don't really understand why. I get basic de/referencing and that implementing the Sized trait means having a known constant size at compile time but don't understand it's use/need in this context.

IIUC, since Deref::Target is ?Sized (i.e., optionally sized) and respond_with_onion_message takes a reference which is Sized, we need to restrict its uses to Sized types.

Copy link
Contributor

Choose a reason for hiding this comment

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

self.custom_handler is a type implementing Deref where the Target implements CustomOnionMessageHandler. respond_with_onion_message wants a reference to a type implementing ResponseErrorHandler, which is a supertrait. It seems just calling deref() would have sufficed.

Oh I see, okay. Reading up a bit more on Deref made me realize there was more happening than I realized. Although, I would've thought Rust would be able to do the type coercion for the supertrait here, hmph.

IIUC, since Deref::Target is ?Sized (i.e., optionally sized) and respond_with_onion_message takes a reference which is Sized, we need to restrict its uses to Sized types.

I tried to add ?Sized on the EH type in respond_with_onion_message and that worked too lol, so I guess for some reason Rust needs us to tell it if it's going to be optionally sized or sized, but I still don't really get why... It doesn't make a difference to the actual code here, so whatever, but weird.

@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch from 238b72e to 5db5cc2 Compare May 24, 2023 19:21
Copy link
Contributor Author

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Whoops, forgot to send these comments.

@@ -209,6 +207,9 @@ mod tests {
let log_entries = logger.lines.lock().unwrap();
assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(),
"Received an onion message with path_id None and no reply_path".to_string())), Some(&1));
// TODO: Add reply path to one_hop_om instead?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@valentinewallace Does this sound right? How was that input generated?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for the delay. I just wrote a test with hardcoded keys + printed out the encoded onion message, I think. Here's the branch I used if it helps: https://github.com/valentinewallace/rust-lightning/tree/2022-10-GEN-OM-FUZZ

Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be nice to get something that manages to reply, indeed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seeing a test failure when cherrypicking this code. I pinged @valentinewallace offline but may need to wait for timezones to catch up.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looked into this. For the test where we're sending directly to a blinded path, we spuriously advance the blinded path on send because in fuzzing all the node pks are the same (so we think we're the intro node to the blinded path).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for helping track this down! I've updated the fuzzer such that each node uses a different public key and was able to generate updated onion messages. Added a separate commit for this.

Also, I updated the fuzzer to test the code path for responding given a reply path. The least intrusive way of doing this was to have the unblinded one-hop case include a one-hop blinded path back. This required modifying BlindedPath::new_for_message to allow such paths. This branch contains the needed modifications for updating the generated onion messages: https://github.com/jkczyz/rust-lightning/tree/2023-02-offer-flow-fuzz

match Self::parse(tlv_type, bytes) {
Ok(message) => Ok(message),
Err(ParseError::Decode(e)) => Err(e),
Err(_) => Err(DecodeError::InvalidValue), // TODO: log ParseError?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Went ahead and made a couple new DecodeError variants. PTAL


let erroneous_field = match (erroneous_field, suggested_value) {
(None, None) => None,
(None, Some(_)) => None,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI, I added an additional error here. Discussion from the spec meeting indicated the error fields are meant to be generic enough for future uses. So might as well support reading them at least.

@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch from 5db5cc2 to f22cea5 Compare May 25, 2023 22:08
@@ -79,6 +80,10 @@ pub enum DecodeError {
Io(io::ErrorKind),
/// The message included zlib-compressed values, which we don't support.
UnsupportedCompression,
/// A BOLT 12 Offers message had invalid semantics.
InvalidSemantics(offers::parse::SemanticError),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't this what InvalidValue is for? Is there a reason we need to have an offers specific error variant in the super general purpose decode failure enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is for logging purposes. See discussion at #2294 (comment).

I could instead make a more general variant wrapping a string or something like Box<dyn Error> to avoid constructing the string if the log level is not hit.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I could instead make a more general variant wrapping a string or something like Box to avoid constructing the string if the log level is not hit.

Sure, we could do something like that. I don't think our serialization framework should have offers-specific code/variants in it, but a String (or &'static str) in an error path seems okay. We also might consider including the (optional) specific TLV that failed to parse as Rusty wants to get that back in errors too now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We also might consider including the (optional) specific TLV that failed to parse as Rusty wants to get that back in errors too now.

Hmm... is that something outside of InvoiceError? There the TLV in question is not necessarily about a parse failure (i.e., DecodeError) but rather some semantic error, FWIW.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt May 30, 2023

Choose a reason for hiding this comment

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

Yea, its not specific to the BOLT12 stuff, we may want it generally, but the invoice_error message has a specific TLV that generated the error - presumably that should be filled in whether the TLV was invalid (eg too many bytes/invalid value) or if it was a semantic error. For that to work it has to be generic in DecodeError and parsing BOLT12 messages have to map InvoiceError to DecodeError.

///
/// [`OnionMessage`]: msgs::OnionMessage
fn find_path(
&self, sender: &PublicKey, destination: Destination
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this need some kind of currently-connected-peers set?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm... perhaps? Currently, this is called from OnionMessenger::respond_with_onion_message via OnionMessenger::handle_onion_message which is passed the id of the peer who delivered the message. So maybe that id is sufficient? Otherwise, I suppose we'd have to pass all the ids to handle_onion_message from PeerManager.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I assume we should - better to find a path with the live peer set rather than just guessing based on the public channels we have (which won't work for private nodes anyway?). I don't think we need to pass a "live" set from PeerManager, just maintaining a copy via peer_{dis,}connected in the messenger should suffice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I missed that we are already maintaining this as the keys in OnionMessenger::pending_messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated find_path to take the peers.

L::Target: Logger,
{
fn handle_message(&self, _message: OffersMessage) -> Option<OffersMessage> {
todo!()
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we can land this with a panic in it? As it stands you can hook this up and just hit panics left and right. We should instead leave IgnoringMessageHandler as the only implementer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just removed the todo! as it was much simpler than changing SimpleArcOnionMessenger.

}
}

impl<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can use AChannelManager here rather than writing it out?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wasn't aware of that, but it looks like it is meant to be test-only.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can just make it public. We did the same for APeerManager.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved everything into channelmanager.rs given all the utilities are there in the next PR. Also, prevents needing to make a bunch of fields pub(crate) then.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Note that AChannelManager was made public yesterday in a separate PR, so its now already usable.

///
/// Each message handler may choose to reply to an onion message with a response. This handler is
/// used when an error occurs when responding.
pub trait ResponseErrorHandler {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, is this API not maybe simpler by simply having the OfferMessageHandler take the router itself and it can figure it out from there? Returning a message to send, then getting an immediate callback that it failed seems like a confusing multi-step process that may require storing additional state, vs just getting that feedback from a router and knowing you failed doesn't.

Further, in the future we may want to use onion message routing as a "free boolean probe" - when sending an invoice_request we may use the normal router seeking a route for the amount we want to send and if the request fails we know we shouldn't pay over that route. That kinda implies the routing has to happen in the message handler itself rather than in the messenger/caller.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, is this API not maybe simpler by simply having the OfferMessageHandler take the router itself and it can figure it out from there? Returning a message to send, then getting an immediate callback that it failed seems like a confusing multi-step process that may require storing additional state, vs just getting that feedback from a router and knowing you failed doesn't.

Hmmm... that pushes the onus on the user to not only do the routing but also to send the message. So we'd have to pass the OnionMessenger, too. Or I suppose we could just pass the OnionMessenger so that the handler can call respond_with_onion_message, which would do the routing using the messenger's MessageRouter. We'd have to also pipe the peer(s) through, too.

Either way, it will expose all OnionMessenger's type parameters to the handler traits. 🙁

FYI, another reason I did it this way is so logging was within OnionMessenger.

Further, in the future we may want to use onion message routing as a "free boolean probe" - when sending an invoice_request we may use the normal router seeking a route for the amount we want to send and if the request fails we know we shouldn't pay over that route. That kinda implies the routing has to happen in the message handler itself rather than in the messenger/caller.

Not quite sure I follow how that would even work. Wouldn't the user parameterize with some custom MessageRouter if they want to do such a probe?

Copy link
Contributor

Choose a reason for hiding this comment

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

So handle_custom_message returns Option<Self::CustomMessage, OnionMessagePath> and then the messenger is just responsible for send_onion_message? Agree that would simplify things - eg if you can't find a route to the intro node, your handler can just give up.

IIUC, I think some form of ResponseErrorHandler would still be needed to notify if send_onion_message fails so that your handler knows what to do next?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmmm... that pushes the onus on the user to not only do the routing but also to send the message. So we'd have to pass the OnionMessenger, too. Or I suppose we could just pass the OnionMessenger so that the handler can call respond_with_onion_message, which would do the routing using the messenger's MessageRouter. We'd have to also pipe the peer(s) through, too.

We could still return the message to be sent, just a routed one rather than the contents only. You'd have to pass it a router but not the messenger itself.

FYI, another reason I did it this way is so logging was within OnionMessenger.

I'm not convinced this should be a goal - it costs nothing to add a logger to the downstream implementation (eg the ChannelManager will already have one), and if it leads to avoiding a lot of strange API it seems worth it?

Not quite sure I follow how that would even work. Wouldn't the user parameterize with some custom MessageRouter if they want to do such a probe?

Oh, I take that, back, this is unrelated. I was thinking about the case where a ChannelManager does the initial invoice_request sending - it would like to be able to pick the path based on its own existing Router, but it can via a manual-path message send.

Copy link
Collaborator

Choose a reason for hiding this comment

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

More broadly, the current PR doesn't actually implement handle_response_error anywhere, and its not super clear to me what we can do with the response error but log anyway? Like, if we receive an invoice_request and fail to send the invoice back, it may be worth logging, but we can't actually do anything - the payment may still complete with a second invoice_request received.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI, another reason I did it this way is so logging was within OnionMessenger.

I'm not convinced this should be a goal - it costs nothing to add a logger to the downstream implementation (eg the ChannelManager will already have one), and if it leads to avoiding a lot of strange API it seems worth it?

The problems isn't with having a logger, but rather with implementations needing to write the code that logs in five places in respond_with_onion_message across however many handler traits there happen to be going forward.

More broadly, the current PR doesn't actually implement handle_response_error anywhere, and its not super clear to me what we can do with the response error but log anyway? Like, if we receive an invoice_request and fail to send the invoice back, it may be worth logging, but we can't actually do anything - the payment may still complete with a second invoice_request received.

Yeah, it was just going to log in the ChannelManager implementation, anyhow. I'm inclined to drop ResponseErrorHandler for now and keep the interface as is. We can always rethink it later if there is some concrete action the user could take.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm inclined to drop ResponseErrorHandler for now and keep the interface as is.

Yea, that makes sense toe me, then.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed ResponseErrorHandler.

///
/// [`OnionMessage`]: msgs::OnionMessage
pub trait MessageRouter {
/// Returns a route for sending an [`OnionMessage`] to the given [`Destination`].
Copy link
Collaborator

Choose a reason for hiding this comment

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

@TheBlueMatt Would you be ok with that or will this make bindings difficult?

Not sure why this would make the bindings difficult?

Copy link
Contributor Author

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

FYI, I'm, going to squash the most recent fixups, otherwise subsequent ones will cause many merge conflicts.

///
/// [`OnionMessage`]: msgs::OnionMessage
fn find_path(
&self, sender: &PublicKey, destination: Destination
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I missed that we are already maintaining this as the keys in OnionMessenger::pending_messages.

@@ -79,6 +80,10 @@ pub enum DecodeError {
Io(io::ErrorKind),
/// The message included zlib-compressed values, which we don't support.
UnsupportedCompression,
/// A BOLT 12 Offers message had invalid semantics.
InvalidSemantics(offers::parse::SemanticError),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We also might consider including the (optional) specific TLV that failed to parse as Rusty wants to get that back in errors too now.

Hmm... is that something outside of InvoiceError? There the TLV in question is not necessarily about a parse failure (i.e., DecodeError) but rather some semantic error, FWIW.

@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch from f22cea5 to 5a1a162 Compare June 1, 2023 02:09
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Conceptually LGTM with one further question about errors.

@@ -79,6 +79,8 @@ pub enum DecodeError {
Io(io::ErrorKind),
/// The message included zlib-compressed values, which we don't support.
UnsupportedCompression,
/// The message had invalid semantics.
InvalidSemantics(String),
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't suffice to generate an InvoiceError, we need the TLV field that failed, is that an issue? Presumably if we fail to read an InvoiceRequest/Invoice we need to be able to map the generic DecodeError (or SemanticError) back into an InvoiceError to send to the peer. I'm okay leaving it as a followup but I'm not entirely clear on the final vision - I assume we'd like to ultimately phase InvalidValue out entirely in favor of a InvalidX { human_readable: String, tlv: Option<u64> }? Can we open an issue and/or leave a comment here describing that? Also, I'm not clear on if this is a good first step towards that, given we don't actually use this yet, does it make more sense to just use InvalidValue for now and then move to something like the above in a followup?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm... currently this variant is only for logging purposes when failing to parse a message because of invalid semantics. I was thinking InvoiceError would only be used after we have a semantically valid message but couldn't handle it for some other reason (e.g., erroring when verifying a message, building an invoice response, or paying an invoice). The spec isn't really clear on this and, to be honest, the TLV portion doesn't seem that well thought out, IMO. My plan was to only use the error message portion.

Also, note that InvoiceError is sent back via an onion message to the originator, not to our peer. We'd need to re-think our packet parsing / message handling code to handle this as you're suggesting (i.e., for DecodeError::InvalidValue) or at very least only parse the message as TLVs and move semantic checks to the handler (?) in order to reply with an InvoiceError. But then we need to expose those TLV types in the handlers where previously they were private.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm... currently this variant is only for logging purposes when failing to parse a message because of invalid semantics.

Why does it need to be a DecodeError then? Can't the OnionMessageHandler log the bolt12::ParseError directly rather than munging it into a DecodeError and then logging in peer_handler?

I was thinking InvoiceError would only be used after we have a semantically valid message but couldn't handle it for some other reason (e.g., erroring when verifying a message, building an invoice response, or paying an invoice). The spec isn't really clear on this and, to be honest, the TLV portion doesn't seem that well thought out, IMO. My plan was to only use the error message portion.

Yea, I have no idea what Rusty was really thinking here - the whole "point to the bad TLV" thing isn't really an actionable field, everyone's just gonna implement by passing the human readable string to the user and calling it a day anyway 🤷. Honestly I'd be okay just never filling it in unless someone actually starts using it in a way that makes a difference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why does it need to be a DecodeError then? Can't the OnionMessageHandler log the bolt12::ParseError directly rather than munging it into a DecodeError and then logging in peer_handler?

Ah, so back in #2294 (comment), I think I may have mistaken Payload for Packet and thus thought the Logger would need to be piped through a handful of ReadableArg implementations. But OnionMessenger handler will directly decode the Payload using onion_utils::decode_next_untagged_hop. I should be able to pass the logger through there. Let me give that a try.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, that sounds good, thanks. Also feel free to rebase when you do so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed and rebased. Should be good for another look now.

}
}

impl<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note that AChannelManager was made public yesterday in a separate PR, so its now already usable.

@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch from 5a1a162 to 68be6f7 Compare June 2, 2023 17:44
@@ -209,6 +207,9 @@ mod tests {
let log_entries = logger.lines.lock().unwrap();
assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(),
"Received an onion message with path_id None and no reply_path".to_string())), Some(&1));
// TODO: Add reply path to one_hop_om instead?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be nice to get something that manages to reply, indeed.

@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch from 68be6f7 to c0b3380 Compare June 6, 2023 19:14
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

The commits up to the last two basically LGTM, obv need some kind of resolution on the fuzzing stuff though.

@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch 2 times, most recently from 74e8559 to c1c8316 Compare June 7, 2023 20:46
@jkczyz
Copy link
Contributor Author

jkczyz commented Jun 7, 2023

Added a fixup for addressing the test compilation errors under no-std.

where
ES::Target: EntropySource,
NS::Target: NodeSigner,
L::Target: Logger + Sized,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sized here is causing problems with a lightning-background-processor doc test with --features=futures that uses Arc<dyn lightning::util::logger::Logger + Send + Sync>.

error[E0277]: the size for values of type `(dyn Logger + Send + std::marker::Sync + 'static)` cannot be known at compilation time
   --> src/lib.rs:529:295
    |
34  | ...MyChannelManager>, my_gossip_sync: Arc<MyGossipSync>, my_logger: Arc<MyLogger>, my_scorer: Arc<MyScorer>, my_peer_manager: Arc<MyPeerManager>) {
    |                                                                                                                               ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
    |
    = help: the trait `Sized` is not implemented for `(dyn Logger + Send + std::marker::Sync + 'static)`
    = note: required because of the requirements on the impl of `OnionMessageHandler` for `OnionMessenger<Arc<KeysManager>, Arc<KeysManager>, Arc<(dyn Logger + Send + std::marker::Sync + 'static)>, IgnoringMessageHandler, IgnoringMessageHandler>`
note: required by a bound in `PeerManager`
   --> /Users/jkczyz/src/rust-lightning/lightning/src/ln/peer_handler.rs:683:15
    |
683 |         OM::Target: OnionMessageHandler,
    |                     ^^^^^^^^^^^^^^^^^^^ required by this bound in `PeerManager`

The problem seems to be that handle_onion_message calls methods that take &L and where L: Logger.

@TheBlueMatt I can fix this by having those methods take &L and where L: Deref, L::Target: Logger. Just seems weird to have a reference to a Deref. Alternatively, keeping L and using where L: Deref + Clone, L::Target: Logger works since both & and Arc implement Clone. Not sure what the canonical way of doing this is, though.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can drop the sizing bounds, this patch compiles for me:

diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs
index c0d28500f..32e8bb09d 100644
--- a/lightning/src/onion_message/messenger.rs
+++ b/lightning/src/onion_message/messenger.rs
@@ -413,7 +413,7 @@ for OnionMessenger<ES, NS, L, MR, OMH, CMH>
 where
        ES::Target: EntropySource,
        NS::Target: NodeSigner,
-       L::Target: Logger + Sized,
+       L::Target: Logger,
        MR::Target: MessageRouter,
        OMH::Target: OffersMessageHandler + Sized,
        CMH::Target: CustomOnionMessageHandler + Sized,
diff --git a/lightning/src/onion_message/offers.rs b/lightning/src/onion_message/offers.rs
index f208fdda7..f82afdd61 100644
--- a/lightning/src/onion_message/offers.rs
+++ b/lightning/src/onion_message/offers.rs
@@ -91,7 +91,7 @@ impl Writeable for OffersMessage {
        }
 }
 
-impl<L: Logger> ReadableArgs<(u64, &L)> for OffersMessage {
+impl<L: Logger + ?Sized> ReadableArgs<(u64, &L)> for OffersMessage {
        fn read<R: Read>(r: &mut R, read_args: (u64, &L)) -> Result<Self, DecodeError> {
                let (tlv_type, logger) = read_args;
                if tlv_type == INVOICE_ERROR_TLV_TYPE {
diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs
index 6f21e9fcd..f532ab585 100644
--- a/lightning/src/onion_message/packet.rs
+++ b/lightning/src/onion_message/packet.rs
@@ -203,7 +203,7 @@ impl<T: CustomOnionMessageContents> Writeable for (Payload<T>, [u8; 32]) {
 }
 
 // Uses the provided secret to simultaneously decode and decrypt the control TLVs and data TLV.
-impl<H: CustomOnionMessageHandler, L: Logger>
+impl<H: CustomOnionMessageHandler, L: Logger + ?Sized>
 ReadableArgs<(SharedSecret, &H, &L)> for Payload<<H as CustomOnionMessageHandler>::CustomMessage> {
        fn read<R: Read>(r: &mut R, args: (SharedSecret, &H, &L)) -> Result<Self, DecodeError> {
                let (encrypted_tlvs_ss, handler, logger) = args;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, that did the trick. Removed the other Sized bounds on the the block while I was at it.

@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch from c1c8316 to d85cccc Compare June 8, 2023 19:36
TheBlueMatt
TheBlueMatt previously approved these changes Jun 8, 2023
Copy link
Contributor

@valentinewallace valentinewallace left a comment

Choose a reason for hiding this comment

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

Will take one more glance tomorrow when the jetlag wears off more, but this looks solid

assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(),
"Responding to onion message with path_id None".to_string())), Some(&1));
assert_eq!(log_entries.get(&("lightning::onion_message::messenger".to_string(),
"Failed responding to onion message with path_id None: TooFewBlindedHops".to_string())), Some(&1));
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not use a valid reply path?

Copy link
Contributor Author

@jkczyz jkczyz Jun 12, 2023

Choose a reason for hiding this comment

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

We have an assumption because of the secret key in do_test that the handling node is the second node (i.e., node[1]), so this is the only realistic reply path. In all the other test cases, the handling node isn't the recipient node, so it wouldn't trigger the responding code path.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hoping we can keep this as is and just change the log assertion once we support one-hop blinded paths.

@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch from 3a316cc to 14b1557 Compare June 12, 2023 21:06
match onion_utils::decode_next_untagged_hop(
onion_decode_ss, &msg.onion_routing_packet.hop_data[..], msg.onion_routing_packet.hmac,
(control_tlvs_ss, &*self.custom_handler, &*self.logger)
) {
Ok((Payload::Receive::<<<CMH as Deref>::Target as CustomOnionMessageHandler>::CustomMessage> {
message, control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { path_id }), reply_path,
}, None)) => {
log_info!(self.logger,
Copy link
Contributor

Choose a reason for hiding this comment

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

Pre-existing, but along the lines of some other comments ISTM we could be spammed by this log

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a final commit for this.

@TheBlueMatt
Copy link
Collaborator

Feel free to squash!

@jkczyz
Copy link
Contributor Author

jkczyz commented Jun 13, 2023

Feel free to squash!

Done!

@jkczyz
Copy link
Contributor Author

jkczyz commented Jun 13, 2023

Ah, gonna rebase since previous CI failures look to be resolved by #2353.

jkczyz added 15 commits June 13, 2023 13:07
If an InvoiceRequest or an Invoice delivered via an onion message cannot
be handled, the recipient should reply with an InvoiceError if a reply
path was given. Define the message and conversion from SemanticError.
In an upcoming commit, messages for BOLT 12 offers are read from the
onion payload. Passing a logger allows for logging semantic errors when
parsing the messages.
BOLT 12 Offers makes use of onion messages to request and respond with
invoices. Add these types and an error type to OnionMessageContents
along with the necessary parsing and encoding.
Add a trait for handling BOLT 12 Offers messages to OnionMessenger and a
skeleton implementation of it for ChannelManager. This allows users to
either provide their own custom handling Offers messages or rely on a
version provided by LDK using stateless verification.
To avoid confusion in the upcoming MessageRouter trait, introduce an
OnionMessagePath struct that wraps the intermediate nodes and the
destination. Use this in OnionMessenger::send_onion_message.
Add a trait for finding routes for onion messages and parameterize
OnionMessenger with it. This allows OnionMessenger to reply to messages
that it handles via one of its handlers (e.g., OffersMessageHandler).
Modify onion message handlers to return an optional response message for
OnionMessenger to reply with.
This will allow for testing onion message replies.
When generating onion message fuzz data, the same public key was used
for each node. However, the code now advances the blinded path if the
sender is the introduction node. Use different node secrets for each
node to avoid this. Note that the exercised handling code is for the
sender's immediate peer.
@jkczyz jkczyz force-pushed the 2023-05-onion-message-replies branch from 13be0a9 to 3fd6b44 Compare June 13, 2023 18:08
@TheBlueMatt TheBlueMatt merged commit 74a9ed9 into lightningdevkit:main Jun 13, 2023
Sign up for free to 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.

7 participants