-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Onion message forwarding #10089
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
base: master
Are you sure you want to change the base?
Onion message forwarding #10089
Conversation
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.
Summary of Changes
Hello @gijswijs, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
This pull request introduces the foundational functionality for forwarding onion messages within the Lightning Network. It integrates a new OnionMessage wire protocol message, enables its routing through the existing Sphinx onion processing infrastructure, and provides RPC endpoints for sending and subscribing to these messages. This work lays the groundwork for future application-layer protocols like BOLT12 offers, allowing for privacy-preserving communication beyond just payments.
Highlights
- Onion Message Wire Protocol: Introduces a new
lnwire.OnionMessagetype to the wire protocol, enabling the transmission of onion-encrypted messages between Lightning Network nodes. This message type is distinct from HTLC-carrying onion packets and is designed for application-layer communication. - Sphinx Onion Processing Integration: Extends the existing Sphinx onion processing logic within
htlcswitch/hopto correctly parse and handleOnionMessagepayloads. This includes adapting the hop iterator to recognize onion messages, extracting forwarding information (such as theNextNodeIDfor node-based routing), and performing message-specific TLV validations. - New RPC Endpoints for Onion Messages: Adds
SendOnionMessageandSubscribeOnionMessagesto the LND RPC API. These new endpoints allow external applications and users to programmatically send onion messages to peers and subscribe to a stream of incoming onion messages, facilitating the development of new privacy-preserving communication features. - Dedicated Message Endpoint and Forwarding Logic: Implements a new
onion_message.OnionEndpointthat integrates with themsgmuxto process incominglnwire.OnionMessages. This endpoint is responsible for decrypting the onion blob, determining if the message is for the local node or needs forwarding, and then either dispatching it to subscribers or relaying it to the next hop in the blinded path. - Enhanced Blinded Path Handling: Introduces specific logic for blinded paths within onion messages, including the addition of
NextNodeIDtoForwardingInfo. This ensures that onion messages, which do not carry payment-related information like CLTV deltas or amounts, are correctly processed and forwarded along their intended blinded routes.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments to provide feedback.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
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.
Code Review
This pull request introduces onion message forwarding, a significant feature that touches multiple parts of the codebase. The changes are generally well-structured, with new functionality encapsulated in the onion_message package and corresponding updates to lnwire, htlcswitch, and the RPC layer. The inclusion of integration tests for both direct and forwarded onion messages is a great addition. I've identified a few issues that need attention: a critical bug in handling dummy hops for onion messages, a high-severity issue with TLV decoding that could lead to panics, and a medium-severity issue regarding message routing logic that could cause confusion and potential bugs. Addressing these will improve the correctness and maintainability of the new functionality.
9157266 to
83a36aa
Compare
83a36aa to
4de5d9e
Compare
953b2d8 to
3f475ce
Compare
3f475ce to
606d9e5
Compare
htlcswitch/hop/forwarding_info.go
Outdated
| // NextNodeID is the public key of the next node in the route. This is | ||
| // used by onion messages that do not necessarily care about the channel | ||
| // ID. | ||
| NextNodeID *btcec.PublicKey |
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 don't think we need to add this to the existing HTLC forwarding fabric at all. The only thing we care about for now is that we have a channel between the target peer (see my proposal on how we can lift this requirement).
In other words, we can make this a competely stand alone sub-system, using the actor package.
I described this in another PR, but we'd have two actors:
- the processor:
- In the
readHandler, we read each onion message off the wire, then send it to the proecssor. - The processor takes the onion message, and transform it to figure out where to send it next. It sends it to an onion endpoint for each peer.
- In the
- The onion endpoint:
- When a peer connects, we make an instance of this actor, but only if it has the feature bit.
- It just takes in the incoming message, and sends the onion message to the direct peer, via an interface.
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.
You know that I love the actor package. But that PR (#9820) hasn't been reviewed yet. Adding yet another PR dependency to the onion messaging epic will make it unlikely that this will be merged anytime soon...
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 we should really aim for not building on top of this logic, because the onion messages shouldn't be piped through the switch, let's push for the actor package
htlcswitch/hop/iterator.go
Outdated
|
|
||
| // isOnionMessage is a flag that indicates whether the iterator is for | ||
| // an onion message. | ||
| isOnionMessage bool |
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.
Same here, generally I think we can reduce the invasiveness of this diff, and make it much easier to review with my suggestion above.
htlcswitch/hop/iterator.go
Outdated
| return nil, routeRole, err | ||
| } | ||
| if !isOnionMessage { | ||
| relayInfo, err := routeData.RelayInfo.UnwrapOrErr( |
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.
Yep, we wouldn't need to tangle with this code at all, just slightly duplicating the existing blinded paths processing code.
onionmessage/onion_endpoint.go
Outdated
| }, | ||
| } | ||
|
|
||
| resps, err := o.onionProcessor.DecodeHopIterators( |
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.
IIUC, this'll also actually cause us to hit disk for replay protection. Thinking about it a bit more: is any replay protection even defined for onion messaging?
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.
Yes, it does. I just piggy-backed on the replay protection that's already in place in lightning-onion: https://github.com/gijswijs/lightning-onion/blob/8c58f1d4431b8d95b7c93d30d97809849d5491e1/sphinx.go#L640-L641
AFAIK there's nothing spec-wise that defines replay protection for onion messaging. I've been struggling with this a bit, but chose to keep the protection we already have.
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 no need for reply protection.
| // forwarding information for an onion message. It contains either | ||
| // next_node_id or short_channel_id for each non-final node. It MAY contain | ||
| // the path_id for the final node. | ||
| bytes encrypted_recipient_data = 5; |
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.
If this is intended for users to consume, shouldn't this be decrypted?
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 we even need this OnionMessageUpdate to be honest.
I needed it so that I could have itests that allowed me to test e2e onion messaging: Use SendOnionMessage to kick things of at Alice's end, and use SubscribeOnionMessages at Dave's end to see if everything comes through as expected.
That being said, if somebody would like to build something on top of onion message support (like LNDK is built on top of SubscribeCustomMessages), you would need SubscribeOnionMessages and you would want this to be decrypted.
peer/brontide.go
Outdated
| OnionMessageServer *subscribe.Server | ||
|
|
||
| // OnionMsgSender is a function that sends an onion message to any peer. | ||
| OnionMsgSender func([33]byte, *btcec.PublicKey, []byte) error |
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.
Why do we need this vs just the existing SendMessage method we have? It can submit just the dest and the wire 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.
Because we need to send it to a different peer. We receive the message from a peer, but we need to forward it to another peer, so we need the brontide instance of that peer. So that's why we drop in the SendOnionMessage from server.go. There we use FindPeerByPubStr and use the SendMessageLazy of that peer. I've never liked it as well, and maybe I've overlooked something, so I'm happy to improve.
|
Since #9868 has been merged, I think this needs to be updated, wanna take a look. |
5a7e61d to
dbb64f4
Compare
In this commit, we add two new fundamental data structures: Future[T] and Promise[T]. A future is a response that might be ready at some point in the future. This is already a common pattern in Go, we just make a type safe wrapper around the typical operations: block w/ a timeout, add a call back for execution, pipeline the response to a new future. A promise is an intent to complete a future. Typically the caller receives the future, and the callee is able to complete the future using a promise.
In this commit, we add the actual Actor implementation. We define a series of types and interfaces, that in concert, describe our actor. An actor has some ID, a reference (used to send messages to it), and also a set of defined messages that it'll accept. An actor can be implemented using a simple function if it's stateless. Otherwise, a struct can implement the Receive method, and handle its internal message passing and state that way.
This commit adds thorough test coverage for the new Mailbox interface and ChannelMailbox implementation. The tests verify correct behavior across various scenarios including successful sends, context cancellation, mailbox closure, and concurrent operations. The test suite specifically validates that the mailbox respects both the caller's context and the actor's context during send and receive operations. This ensures that actors properly shut down when their context is cancelled, and that callers can cancel operations without affecting the actor's lifecycle. Additional tests cover edge cases such as zero-capacity mailboxes (which default to a capacity of 1), draining messages after closure, and concurrent sends from multiple goroutines. The concurrent test uses 10 senders each sending 100 messages to verify thread-safety and proper message ordering. All tests pass with the race detector enabled, confirming the implementation is free from data races.
This commit refactors the Actor implementation to use the new Mailbox interface instead of directly managing a channel. This change significantly simplifies the actor's message processing loop and improves separation of concerns. The main changes include replacing the direct channel field with a Mailbox interface, updating NewActor to create a ChannelMailbox instance, and refactoring the process method to use the iterator pattern provided by mailbox.Receive. The new implementation uses a clean for-range loop over the mailbox's message iterator, eliminating the complex select statement that previously handled both message reception and context cancellation. The Tell and Ask methods in actorRefImpl have been simplified to use the mailbox's Send method, which internally handles both the caller's context and the actor's context. This eliminates the need for complex select statements in these methods and ensures consistent context handling throughout the actor system. Message draining during shutdown is now handled through the mailbox's Drain method, providing a cleaner separation between normal message processing and cleanup operations. The actor still properly sends unprocessed messages to the Dead Letter Office and completes pending promises with appropriate errors during shutdown.
86fd4b7 to
57eb251
Compare
dbb64f4 to
e1738ce
Compare
Abdulkbk
left a comment
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.
gone through this again, left a few comments 🙏
| // EncryptedData contains encrypted data for the recipient. | ||
| EncryptedData []byte | ||
|
|
||
| // FinalHopPayloads contains any tlvs with type > 64 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.
incomplete comment. Also type >= 64 since it is inclusive.
| } | ||
|
|
||
| if err := tlv.EPubKey(w, &p.BlindingPoint, buf); err != nil { | ||
| return fmt.Errorf("encode blinded path: %w", err) |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
htlcswitch/hop/iterator.go
Outdated
| }) | ||
| if pathID == nil { | ||
|
|
||
| // If this is not an onion message, then we expect the path ID to be |
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.
Am I misinterpreting the bolt?, or does it seem like for the final node we can expect pathid to be either set or not?
htlcswitch/hop/iterator.go
Outdated
| fmt.Errorf("relay info not set for non-final blinded " + | ||
| "hop"), | ||
| ) | ||
| cltvExpiryDelta = uint32(relayInfo.Val.CltvExpiryDelta) |
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: the error check should come before this.
htlcswitch/hop/iterator.go
Outdated
| blindingKit := BlindingKit{ | ||
| Processor: r.router, | ||
| UpdateAddBlinding: tlv.SomeRecordT( | ||
| tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType]( //nolint:ll |
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: can remove //nolint:ll
|
|
||
| // OnionMessagesRequired is a required feature bit that indicates that | ||
| // the node can forward onion messages. | ||
| OnionMessagesRequired = 39 |
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 "even" should be the required bit right?
| // blinded routes by default. | ||
| OnionBlob []byte | ||
|
|
||
| CustomRecords record.CustomSet |
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: go doc for CustomRecords, ReplyPath, and EncryptedRecipientData.
|
|
||
| // EncryptedRecipientDataType is the type used in the onion message to | ||
| // include encrypted data in the onion for use in blinded paths. | ||
| EncryptedRecipientDataType tlv.Type = 4 |
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.
Wouldn't it make sense to use the same variable names for these as we do in onion_msg_payload for consistency?
57eb251 to
6fcdbbb
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.
Nice to finally being able to forward onionmessages.
My main points are:
- I agree with Laolu here, we should definitly not move this logic into the switch.
- You mention in this PR that its all about forwarding onion_message, however in your code logic you add all the decoding and encoding of the finalHopPayload TLVs (including the replyPath) from my understanding this is all not needed. We only want to support forwarding, which means we only care about the Encrypted_Recipient_Data in this PR and treat all the other TLV types as not known. This helps to keep the PR way smaller and we would only introduce the other types when needed.
|
|
||
| const ( | ||
| // finalHopPayloadStart is the inclusive beginning of the tlv type | ||
| // range that is reserved for payloads for the final hop. |
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 spec however says in case they are odd intermediate nodes don't need to fail it how are we planning to do it:
Field numbers 64 and above are reserved for payloads for the final hop, though these are not explicitly refused by non-final hops (unless even, of course!).
| return &OnionMessagePayload{} | ||
| } | ||
|
|
||
| // Encode encodes an onion message's final payload. |
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 the comment is off it encodes thee whole OnionMessagePacket ?
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: Moreover I think we should add a comment to funcions which are part of an interface something like:
// This is part of the lnwire.Message interface
htlcswitch/hop/forwarding_info.go
Outdated
| // NextNodeID is the public key of the next node in the route. This is | ||
| // used by onion messages that do not necessarily care about the channel | ||
| // ID. | ||
| NextNodeID *btcec.PublicKey |
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 we should really aim for not building on top of this logic, because the onion messages shouldn't be piped through the switch, let's push for the actor package
onionmessage/onion_endpoint.go
Outdated
| }, | ||
| } | ||
|
|
||
| resps, err := o.onionProcessor.DecodeHopIterators( |
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 no need for reply protection.
| EncryptedData []byte | ||
|
|
||
| // FinalHopPayloads contains any tlvs with type > 64 that | ||
| FinalHopPayloads []*FinalHopPayload |
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 we should add ExtraOpaqueData in this struct
| } | ||
|
|
||
| // Decode decodes an onion message's payload. | ||
| func (o *OnionMessagePayload) Decode(r io.Reader) (*OnionMessagePayload, |
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 do not understand this function signature, you change the pointer receiver and then you return it, that looks awkward ?
| } | ||
|
|
||
| // Create a primitive record that just writes the final hop | ||
| // payload's bytes directly. The creating function should have |
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: what do you mean by "directly" here ?
| finalHopPayloadStart tlv.Type = 64 | ||
|
|
||
| // replyPathType is a record for onion messaging reply paths. | ||
| replyPathType tlv.Type = 2 |
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.
So accroding to he spec the reply path does only make sense for the final hop, however it is not in the range of the final hp tlv type. Wonder what the design design here was ?
| } | ||
|
|
||
| // ReplyPath is a blinded path used to respond to onion messages. | ||
| type ReplyPath struct { |
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.
why not just use:
type ReplyPath sphinx.BlindedPath ?
| */ | ||
| rpc SubscribeOnionMessages (SubscribeOnionMessagesRequest) | ||
| returns (stream OnionMessage); | ||
| returns (stream OnionMessageUpdate); |
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.
could you explain to me why you changed this to Update ?
6b32c24 to
64828aa
Compare
706b22a to
95b84a8
Compare
|
|
||
| // Open channels: Alice --- Bob --- Carol and wait for each node to | ||
| // sync the network graph. | ||
| aliceBobChanPoint := ht.OpenChannel( |
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 was considering adding an itest to verify that a channel isn't required, but I think we can probably just have a single channel from Alice -> Bob here. The expected behavior will remain the same, but at least it demonstrates that a channel isn't necessary.
378cc05 to
271a1d6
Compare
e25d3a6 to
e6725f8
Compare
* actor-mailbox-v2: actor: refactor Actor to use Mailbox interface actor: add tests for mailbox implementation actor: introduce generic Mailbox interface with iter.Seq support actor: add README.md actor: add example files actor: add the actor system and router actor: add fundamental interfaces and concrete Actor impl actor: add Future[T] and Promise[T] w/ concrete impls actor: add new actor package as distinct sub-module
The new wire message defines the OnionMessagePayload, FinalHopPayload, ReplyPath, and related TLV encoding/decoding logic.
Update lightning-onion to commit that includes onion-messaging support.
e6725f8 to
f887885
Compare
Adds the NewNonFinalBlindedRouteDataOnionMessage function to create blinded route data specifically for onion messages.
Initialize a sphinx router without persistent replay protection logging for onion message processing. Onion messages don't require replay protection since they're connectionless and don't involve payment routing.
Introduce OnionPeerActor to handle the sending of onion message to each peer. The actor is registered with the receptionist pattern to enable message routing through the actor system. Also adds onion message feature bits to the protocol, so that the actor is only spawned when the peer supports onion messages.
Add onion message forwarding capability using the OnionPeerActor for communication. Messages are routed through a receptionist pattern where each peer has a dedicated OnionPeerActor for handling message sends. The OnionEndpoint uses the sphinx router for decoding and decrypting the onion message packet and the encrypted recipient data in the payload of the onion messages.
Change SubscribeOnionMessages RPC to return OnionMessageUpdate instead of OnionMessage, including the decrypted payload to enable payload inspection in integration tests. The encrypted recipient data is still not decrypted here.
Verify that onion messages can be correctly forwarded through a multi-hop path by testing the complete flow from sender to recipient through intermediate nodes.
f887885 to
6d9c71a
Compare
|
@gijswijs, remember to re-request review from reviewers when ready |
With this PR we add basic forwarding functionality for onion messages. It builds on PR #9868.
It adds
OnionMessagePayloadstruct to thelnwirepackage.It also depends on the not yet merged (PR 68)[https://github.com/lightningnetwork/lightning-onion/pull/68] in the
lightning-onionpackage. For now it uses that package from a forked version.The
msgmuxendpoint for onion messages is updated to parse the onion message packet, and forward the onion based on the acquired information.The
SubscribeOnionMessagesendpoint is updated to pass along any decrypted information. This endpoint is currently solely meant for itests, although it could have practical use in the future.