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

Remove message type bound on ResponseInstruction #3263

Merged
merged 9 commits into from
Aug 23, 2024

Conversation

TheBlueMatt
Copy link
Collaborator

Our onion message handlers generally have one or more methods which return a ResponseInstruction parameterized with the expected message type (enum) of the message handler.

Sadly, that restriction is impossible to represent in our bindings - our bindings concretize all LDK structs, enums, and traits into a single concrete instance with generics set to our concrete trait instances (which hold a jump table). This prevents us from having multiple instances of ResponseInstruction structs for different message types.

Our bindings do, however, support different parameterizations of standard enums, including Options and tuples.

In order to support bindings for the onion message handlers, we are thus forced into std types bound by expected message types, which we do here by making ResponseInstruction contain only the instructions and generally using it as a two-tuple of (message, ResponseInstruction).

I'm torn on whether this belongs upstream - its not quite as nice an API, but its really not all that much different, so it seems kinda fine. We could also make this a bindings-specific change, but that's yet more diff for the bindings, in an area that is set to grow over time as we use OMs more.

@TheBlueMatt TheBlueMatt added this to the 0.0.124 milestone Aug 21, 2024
@valentinewallace
Copy link
Contributor

I'm good with upstreaming this (actually prefer None over ResponseInstruction::NoResponse) but will need @jkczyz's take.

Copy link

codecov bot commented Aug 21, 2024

Codecov Report

Attention: Patch coverage is 85.24590% with 27 lines in your changes missing coverage. Please review.

Project coverage is 90.01%. Comparing base (bbfa15e) to head (47b527a).
Report is 24 commits behind head on main.

Files Patch % Lines
lightning/src/ln/channelmanager.rs 79.54% 9 Missing ⚠️
lightning/src/ln/offers_tests.rs 78.57% 6 Missing ⚠️
lightning/src/ln/peer_handler.rs 16.66% 5 Missing ⚠️
lightning/src/onion_message/functional_tests.rs 91.48% 4 Missing ⚠️
lightning/src/onion_message/messenger.rs 96.42% 2 Missing ⚠️
lightning/src/onion_message/async_payments.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3263      +/-   ##
==========================================
+ Coverage   89.82%   90.01%   +0.19%     
==========================================
  Files         125      125              
  Lines      102867   106409    +3542     
  Branches   102867   106409    +3542     
==========================================
+ Hits        92398    95782    +3384     
- Misses       7753     7976     +223     
+ Partials     2716     2651      -65     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@jkczyz
Copy link
Contributor

jkczyz commented Aug 21, 2024

Yeah, I'm fine with this change

@TheBlueMatt
Copy link
Collaborator Author

Okay, fixed the excess commit and build....and then got carried away and fixed #3178 and cleaned up the onion message sending API a bit.

@TheBlueMatt TheBlueMatt linked an issue Aug 21, 2024 that may be closed by this pull request
Comment on lines 368 to 371
pub fn respond<T: OnionMessageContents>(self, response: T) -> Option<(T, ResponseInstruction)> {
Some((response, ResponseInstruction::WithoutReplyPath {
send_path: self.reply_path,
}))
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we drop Option from here and have the caller wrap if this will always be Some? The interface is also a little strange in that the message is passed only to be returned. Probably don't need to pass it at all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yea, I was thinking it makes the calling code a bit more readable to return the exact type they need to return, but you're right its a bit awkward to take a parameter which is just immediately returned...I'll swap it.

lightning/src/onion_message/messenger.rs Outdated Show resolved Hide resolved
lightning/src/onion_message/messenger.rs Outdated Show resolved Hide resolved
lightning/src/onion_message/messenger.rs Outdated Show resolved Hide resolved
@jkczyz
Copy link
Contributor

jkczyz commented Aug 21, 2024

Okay, fixed the excess commit and build....and then got carried away and fixed #3178 and cleaned up the onion message sending API a bit.

Oops, was reviewing before you pushed. Will take another look.

Comment on lines 49 to 48
&self, message: OffersMessage, context: Option<OffersContext>, responder: Option<Responder>,
) -> Option<(OffersMessage, ResponseInstruction)>;
) -> Option<(OffersMessage, MessageSendInstructions)>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm... the purpose of Responder was to ensure that the response is only sent over the reply path. But this change makes it possible for a handler to respond over any destination (blinded or not).

It would be preferable to still have something like OnionMessageResponse which is used in the handler interfaces and can only be created by Responder since it is not publicly constructible. And then still have a MessageSendInstructions for the more general interface but with a variant that wraps OnionMessageResponse.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmmm... the purpose of Responder was to ensure that the response is only sent over the reply path. But this change makes it possible for a handler to respond over any destination (blinded or not).

I don't think that's a terrible thing? I mean basically it would no longer be a response but rather just a message they're sending, but, like, okay, if they want to do that 🤷‍♂️.

Copy link
Contributor

Choose a reason for hiding this comment

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

Responder is kinda useless then if you don't need it to construct instructions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yea, kinda? I mean it does give a type description and make the code a bit more readable, but you're right that it could be fully removed and wouldn't change too much. I don't think that's a reason why we should introduce more wrapping to make it useful, though. If you'd rather see it removed happy to do that too, though.

Copy link
Contributor

Choose a reason for hiding this comment

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

How about keeping ResponseInstructions for the handlers and converting it into MessageSendInstructions inside `handle_onion_message_response? Then there is no wrapping.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right, but still near-identical. Not quite sure I understand the motivation for adding a new type just to restrict the cases that the user doesn't even deal with?

Copy link
Contributor

Choose a reason for hiding this comment

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

The motivation is to enforce the correct behavior of only using the reply path for the response. And it prevents users from doing something wrong like manually creating a MessageSendInstructions to return when the responder is None.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The motivation is to enforce the correct behavior of only using the reply path for the response.

You mean only using the reply path we were given in the message to respond to that message? Is the concern there that someone would accidentally use that reply path to send another message or to receive some message (treating it as our own reply path)? I'm not sure how likely that is, though maybe leaving the "return an identical type from respond as the method returns" thing would help there (without having to define yet another type)?

And it prevents users from doing something wrong like manually creating a MessageSendInstructions to return when the responder is None.

I mean in order to do so they'd have to fill in the Destination with something, which seems like it'd be pretty hard to get wrong?

Copy link
Contributor

Choose a reason for hiding this comment

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

You mean only using the reply path we were given in the message to respond to that message? Is the concern there that someone would accidentally use that reply path to send another message or to receive some message (treating it as our own reply path)? I'm not sure how likely that is, though maybe leaving the "return an identical type from respond as the method returns" thing would help there (without having to define yet another type)?

It's more so having them be confused about how to create the return value and think they need to create a Destination themselves (and thus either give it a node id or create a blinded path) when they should use the Responder to create it. If the return type is ResponseInstructions and the only way to create one is through Responder, then they can't screw it up.

Though I guess ResponseInstructions can't have a OnionMessageResponse (because it has a type parameter). So instead of a nearly identical enum, it would need to be a struct with private constructors:

pub struct ResponseInstructions {
    destination: BlindedMessagePath,
    include_reply_path: bool,
}

I mean in order to do so they'd have to fill in the Destination with something, which seems like it'd be pretty hard to get wrong?

The point is they shouldn't be doing that at all, but our interface allows for it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, so I think I agree with you, but in order to fix #3178 we end up with a ForResponse variant in MessageSendInstructions which I find a bit weird, but is tolerable.

lightning/src/onion_message/messenger.rs Outdated Show resolved Hide resolved
@TheBlueMatt TheBlueMatt force-pushed the 2024-08-bindings-om branch 4 times, most recently from f59c9c0 to d577b24 Compare August 22, 2024 02:03
lightning/src/onion_message/messenger.rs Outdated Show resolved Hide resolved
Comment on lines 425 to 432
/// Indicates that a message is being sent as a reply to a received message.
ForReply {
instructions: ResponseInstruction,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't into_instructions sufficient?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, I guess we could make that public. I was thinking of leaving it private to differentiate the public API which seemed like part of the goal. This would (though I didn't bother yet) allow us to log differently if its a reply, but I'm not sure if that's really worth changing the API for. Don't really feel strongly here at all, happy to do whatever you think makes more sense.

Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, would having "async" versions of the Responder methods returning the appropriate MessageSendInstructions be too confusing? Otherwise, the extra variant here is also fine, IMO.

Copy link
Collaborator Author

@TheBlueMatt TheBlueMatt Aug 22, 2024

Choose a reason for hiding this comment

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

Hmm, I guess we could? I kinda prefer the other two options, though, "async" versions just seems like more stuff users have to think about before they use Responder.

message: response,
reply_path: self.reply_path,
}, context)
pub fn respond_with_reply_path(self, context: MessageContext) -> ResponseInstruction {
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this is only used in tests atm. Are we keeping it public for non-LDK use, or for future use, or?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yea, for non-LDK use.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

And probably future use...

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this is used in #3163

@valentinewallace
Copy link
Contributor

LGTM when jeff's happy

Copy link
Contributor

@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.

LGTM modulo some minor comments. Feel feee to squash.

lightning/src/onion_message/messenger.rs Show resolved Hide resolved
lightning/src/onion_message/messenger.rs Outdated Show resolved Hide resolved
@TheBlueMatt
Copy link
Collaborator Author

Squashed and addressed last nits:

$ git diff-tree -U5 2ddaffd3d f43e85ec1
diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs
index a33ee4f27..7d215e365 100644
--- a/lightning/src/onion_message/messenger.rs
+++ b/lightning/src/onion_message/messenger.rs
@@ -384,10 +384,13 @@ impl Responder {
 }

 /// Instructions for how and where to send the response to an onion message.
 #[derive(Clone)]
 pub struct ResponseInstruction {
+	/// The destination in a response is always a [`Destination::BlindedPath`] but using a
+	/// [`Destination`] rather than an explicit [`BlindedMessagePath`] simplifies the logic in
+	/// [`OnionMessenger::send_onion_message_internal`] somewhat.
 	destination: Destination,
 	context: Option<MessageContext>,
 }

 impl ResponseInstruction {
@@ -1157,12 +1160,13 @@ where
 		&self, contents: T, instructions: MessageSendInstructions, log_suffix: fmt::Arguments,
 	) -> Result<SendSuccess, SendError> {
 		let (destination, reply_path) = match instructions {
 			MessageSendInstructions::WithSpecifiedReplyPath { destination, reply_path } =>
 				(destination, Some(reply_path)),
-			MessageSendInstructions::WithReplyPath { destination, context }|
-			MessageSendInstructions::ForReply { instructions: ResponseInstruction { destination, context: Some(context) } } => {
+			MessageSendInstructions::WithReplyPath { destination, context }
+				|MessageSendInstructions::ForReply { instructions: ResponseInstruction { destination, context: Some(context) } } =>
+			{
 				match self.create_blinded_path(context) {
 					Ok(reply_path) => (destination, Some(reply_path)),
 					Err(err) => {
 						log_trace!(
 							self.logger,
@@ -1171,12 +1175,12 @@ where
 						);
 						return Err(err);
 					}
 				}
 			},
-			MessageSendInstructions::WithoutReplyPath { destination }|
-			MessageSendInstructions::ForReply { instructions: ResponseInstruction { destination, context: None } } =>
+			MessageSendInstructions::WithoutReplyPath { destination }
+				|MessageSendInstructions::ForReply { instructions: ResponseInstruction { destination, context: None } } =>
 				(destination, None),
 		};

 		let mut logger = WithContext::from(&self.logger, None, None, None);
 		let result = self.find_path(destination).and_then(|path| {

@TheBlueMatt
Copy link
Collaborator Author

Oops, missed one more:

$ git diff-tree -U2 f43e85ec1 d09e7320f
diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs
index a0e56cdc5..279d9fddb 100644
--- a/lightning/src/onion_message/async_payments.rs
+++ b/lightning/src/onion_message/async_payments.rs
@@ -12,5 +12,4 @@
 use crate::io;
 use crate::ln::msgs::DecodeError;
-#[cfg(not(c_bindings))]
 use crate::onion_message::messenger::{Responder, ResponseInstruction, MessageSendInstructions};
 use crate::onion_message::packet::OnionMessageContents;

jkczyz
jkczyz previously approved these changes Aug 22, 2024
Our onion message handlers generally have one or more methods which
return a `ResponseInstruction` parameterized with the expected
message type (enum) of the message handler.

Sadly, that restriction is impossible to represent in our bindings -
our bindings concretize all LDK structs, enums, and traits into a
single concrete instance with generics set to our concrete trait
instances (which hold a jump table). This prevents us from having
multiple instances of `ResponseInstruction` structs for different
message types.

Our bindings do, however, support different parameterizations of
standard enums, including `Option`s and tuples.

In order to support bindings for the onion message handlers, we
are thus forced into std types bound by expected message types,
which we do here by making `ResponseInstruction` contain only the
instructions and generally using it as a two-tuple of
`(message, ResponseInstruction)`.
In the coming commits we'll use the `ResponseInstruction` enum's
contents for all messages, allowing message senders to rely on the
in-`OnionMessegner` reply path selection logic.

In order to do so and avoid users confusing the new
`MessageSendInstructions` for `ResponseInstruction`, we leave
`ResponseInstruction` as a now-unconstructible struct which wraps a
`MessageSendInstructions`.
In the next commit we'll use `MessageSendInstructions` for all
messages, so it needs the ability to take any `Destination`, not
only a `Destination::Blinded`.
Now that the `MessageRouter` can `create_blinded_paths` forcing
callers of the `OnionMessenger` to provide it with a reply path up
front is unnecessary complexity, doubly so in message handlers.

Here we take the first step towards untangling that, moving from
`PendingOnionMessage` to `MessageSendInstructions` for the outbound
message queue in `CustomMessageHandler`. Better, we can also drop
the `c_bindings`-specific message queue variant, unifying the APIs.
Now that the `MessageRouter` can `create_blinded_paths` forcing
callers of the `OnionMessenger` to provide it with a reply path up
front is unnecessary complexity, doubly so in message handlers.

Here we take the next step towards untangling that, moving from
`PendingOnionMessage` to `MessageSendInstructions` for the outbound
message queue in `OffersMessageHandler`. Better, we can also drop
the `c_bindings`-specific message queue variant, unifying the APIs.

Because `ChannelManager` needs to actually control the reply path
set in individual messages, however, we have to halfway this patch,
adding a new `MessageSendInstructions` variant that allows
specifying the `reply_path` explicitly. Still, because other message
handlers are moving this way, its nice to be consistent.
Now that the `MessageRouter` can `create_blinded_paths` forcing
callers of the `OnionMessenger` to provide it with a reply path up
front is unnecessary complexity, doubly so in message handlers.

Here we take the next step towards untangling that, moving from
`PendingOnionMessage` to `MessageSendInstructions` for the outbound
message queue in `AsyncPaymentsMessageHandler`. Better, we can also
drop the `c_bindings`-specific message queue variant, unifying the
APIs.

Here we also drop `PendingOnionMessage` entirely.
This lets callers include a `reply_path` without doing the
path-finding at the callsite, utilizing the built-in path-finding
logic in `OnionMessenger`
In order to allow onion message handlers to reply asynchronously
without introducing a circular dependency graph, the message
handlers need to be able to send replies described by
`MessageSendInstructions`. This allows them to send replies via the
normal message queuing (i.e. without making a function call to
`OnionMessenger`).

Here we enable that by adding a `MessageSendInstructions::ForReply`
variant which holds `ReplyInstruction`s.

Fixes lightningdevkit#3178
@TheBlueMatt
Copy link
Collaborator Author

Grrrrr, screwed up the fuzzers:

$ git diff-tree -U1 d09e7320f 47b527a65
diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs
index 0de791c55..33458436c 100644
--- a/fuzz/src/onion_message.rs
+++ b/fuzz/src/onion_message.rs
@@ -18,4 +18,4 @@ use lightning::onion_message::async_payments::{
 use lightning::onion_message::messenger::{
-	CustomOnionMessageHandler, Destination, MessageRouter, OnionMessagePath, OnionMessenger,
-	PendingOnionMessage, Responder, ResponseInstruction,
+	CustomOnionMessageHandler, Destination, MessageRouter, MessageSendInstructions,
+	OnionMessagePath, OnionMessenger, Responder, ResponseInstruction,
 };
@@ -111,4 +111,4 @@ impl OffersMessageHandler for TestOffersMessageHandler {
 		_responder: Option<Responder>,
-	) -> ResponseInstruction<OffersMessage> {
-		ResponseInstruction::NoResponse
+	) -> Option<(OffersMessage, ResponseInstruction)> {
+		None
 	}
@@ -121,9 +121,11 @@ impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler {
 		&self, message: HeldHtlcAvailable, responder: Option<Responder>,
-	) -> ResponseInstruction<ReleaseHeldHtlc> {
+	) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> {
 		let responder = match responder {
 			Some(resp) => resp,
-			None => return ResponseInstruction::NoResponse,
+			None => return None,
 		};
-		responder
-			.respond(ReleaseHeldHtlc { payment_release_secret: message.payment_release_secret })
+		Some((
+			ReleaseHeldHtlc { payment_release_secret: message.payment_release_secret },
+			responder.respond(),
+		))
 	}
@@ -160,6 +162,6 @@ impl CustomOnionMessageHandler for TestCustomMessageHandler {
 		responder: Option<Responder>,
-	) -> ResponseInstruction<Self::CustomMessage> {
+	) -> Option<(Self::CustomMessage, ResponseInstruction)> {
 		match responder {
-			Some(responder) => responder.respond(message),
-			None => ResponseInstruction::NoResponse,
+			Some(responder) => Some((message, responder.respond())),
+			None => None,
 		}
@@ -173,3 +175,3 @@ impl CustomOnionMessageHandler for TestCustomMessageHandler {
 	}
-	fn release_pending_custom_messages(&self) -> Vec<PendingOnionMessage<Self::CustomMessage>> {
+	fn release_pending_custom_messages(&self) -> Vec<(TestCustomMessage, MessageSendInstructions)> {
 		vec![]
diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs
index 279d9fddb..37e4a5341 100644
--- a/lightning/src/onion_message/async_payments.rs
+++ b/lightning/src/onion_message/async_payments.rs
@@ -13,3 +13,3 @@ use crate::io;
 use crate::ln::msgs::DecodeError;
-use crate::onion_message::messenger::{Responder, ResponseInstruction, MessageSendInstructions};
+use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction};
 use crate::onion_message::packet::OnionMessageContents;

@TheBlueMatt TheBlueMatt merged commit 49dfa5a into lightningdevkit:main Aug 23, 2024
18 of 19 checks passed
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.

Onion Message Responder creates a circular reference
3 participants