Skip to content

Onion messaging support #68

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

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

gijswijs
Copy link

@gijswijs gijswijs commented Jul 8, 2025

This PR adds support for onion messaging to the lightning-onion package. It does the following things:

  • pulls in the test vectors from the BOLT 4 spec for onion messaging
  • adds test TestBuildOnionMessageBlindedRoute that tests BuildBlindedRoute, decryptBlindedHopData and NextEphemeral against this vector.
  • add test TestOnionMessageRouteBlinding which verifies that the onion message packet from the test vector can be processed correctly by the nodes in a blinded route.
  • add test TestTLVPayloadMessagePacket that creates a onion message with payload and the blinded route from the test vector.

It also adds support for zero-length payloads and longer than 1300 bytes payloads that are specifically supported by the spec, but not tested by the test vectors supplied. Both features are accompanied by their own unit tests.

Copy link

@gemini-code-assist gemini-code-assist bot left a 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 significantly extends the lightning-onion package by integrating full support for onion messaging, a distinct type of onion routing. The changes involve fundamental modifications to how onion packets are structured, created, and processed, enabling greater flexibility in payload sizes and strict adherence to TLV encoding for messages. This enhancement is crucial for future-proofing the library and supporting advanced messaging capabilities within the Lightning Network.

Highlights

  • Onion Messaging Support: This pull request introduces comprehensive support for 'onion messaging' within the lightning-onion package, distinguishing it from traditional payment onions. This includes new packet structures, creation, and processing logic tailored for onion messages.
  • Flexible Routing Info Sizes: The fixed-size RoutingInfo field in OnionPacket has been replaced with a dynamic slice ([]byte), enabling support for variable payload sizes. New constants StandardRoutingInfoSize (1300 bytes) and JumboRoutingInfoSize (32768 bytes) are introduced to accommodate different onion message lengths, including those exceeding the previous 1300-byte limit.
  • Payload Type Enforcement: Onion messages are now strictly enforced to use TLV (Type-Length-Value) payloads, aligning with BOLT 7 specifications. The packet creation and processing logic has been updated to validate this, rejecting legacy payloads for onion messages.
  • Updated Packet Creation and Processing: The NewOnionPacket and ProcessOnionPacket functions now accept functional options (OnionPacketOption, ProcessOnionOpt) to specify if the packet is an onion message. This allows for specialized handling, such as dynamic routing info sizing and payload type validation, based on the packet's purpose.
  • New Test Vectors and Comprehensive Testing: New test vectors from the BOLT 4 spec for onion messaging have been integrated. Dedicated tests (TestBuildOnionMessageBlindedRoute, TestOnionMessageRouteBlinding, TestTLVPayloadMessagePacket) have been added to validate blinded route construction, packet processing, and TLV payload handling for onion messages. Tests for zero-length and larger than 1300-byte payloads are also included.
  • CLI Tool Enhancement: A new --onion-message flag has been added to the cmd/main.go CLI tool, allowing users to generate onion message packets directly.
  • Dependency Updates: Several Go module dependencies, including btcd, btcec/v2, stretchr/testify, and golang.org/x/crypto, have been updated to their latest versions. New indirect dependencies like lnd/tlv have also been added.
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

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

@gijswijs
Copy link
Author

gijswijs commented Jul 8, 2025

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a 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 comprehensive support for onion messaging. The core logic changes are well-implemented. The review focuses on improving the new tests, identifying areas for improvement such as code clarity and test correctness.

Copy link

@gemini-code-assist gemini-code-assist bot left a 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 support for onion messaging, including handling of blinded routes and variable-sized payloads. The changes include new logic for packet creation and processing, and are supported by test vectors and new unit tests. I have a few suggestions to improve code clarity and robustness.

gijswijs added 8 commits July 9, 2025 09:45
This commit removes an unused var and changes bytes.Compare to the
idiomatic bytes.Equal.
This commit adds the spec test vector for blinded onion messages. It
also adds a test that tests BuildBlindedRoute, decryptBlindedHopData and
NextEphemeral against this vector.
We add TestOnionMessageRouteBlinding which verifies that the onion
message packet from the test vector can be processed correctly by the
nodes in a blinded route.
TestTLVPayloadMessagePacket creates a onion message with payload and the
blinded route from the test vector. It then checks if the onion packet
we create is equal to the one provided in the test vector.
Since the onion message payload can be zero-length, we need to decode it
correctly. This commit adds a boolean flag to the HopPayload Decode that
tells whether the payload is an onion message payload or not. If it is,
the payload is decoded as a tlv payload also if the first byte is 0x00.
Onion messages allow for payloads that exceed 1300 bytes, in which case
the payload should become 32768 bytes. This commit introduces support
for those jumbo packets.
This commit adds a helper function to create onion messages of a
specified length. This helper is then used to test the handling of
packets larger than 1300 bytes specifically for onion messages.
gijswijs added 4 commits July 10, 2025 15:20
When we are parsing onion messages, we must ensure that a blinding point
is provided.
To temporarily use this package from my own repo, we redeclare the
package path.
The field MaxPayloadSize is added to the sphinx package to allow for
backwards compatibility with the old sphinx package. Removing it would
have been a breaking change.
To provide backwards compatibility, the Decode function in sphinx.go has
been modified to accept an optional isMessage parameter. This is now not
a breaking change.
@saubyk saubyk added this to lnd v0.20 Jul 10, 2025
@saubyk saubyk moved this to In progress in lnd v0.20 Jul 15, 2025
@saubyk
Copy link
Collaborator

saubyk commented Jul 15, 2025

cc: @yyforyongyu @ellemouton for review

Copy link
Member

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

Nice tests! Finish one round, mostly on the code format and tests, will do another round soon to load the context.

@@ -106,8 +106,7 @@ func newTestRoute(numHops int) ([]*Router, *PaymentPath, *[]HopData, *OnionPacke

func TestBolt4Packet(t *testing.T) {
var (
route PaymentPath
hopsData []HopData
route PaymentPath
Copy link
Member

Choose a reason for hiding this comment

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

nit: the commit msg is a bit off sphinx: remove dead code and

path_test.go Outdated
type onionMessageJsonTestCase struct {
Generate generateOnionMessageData `json:"generate"`
Route routeOnionMessageData `json:"route"`
// OnionMessage onionMessageData `json:"onionmessage"`
Copy link
Member

Choose a reason for hiding this comment

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

not used?

// ID as the next node id in the payload of the previous
// node, so we get that from the previous hop.
nodeIDStr, _ := hex.DecodeString(
currentHop,
Copy link
Member

Choose a reason for hiding this comment

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

nit: can fit in one line

}

// First, Dave will build a blinded path from Bob to itself.
DaveSessKey := privKeyFromString(
Copy link
Member

Choose a reason for hiding this comment

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

should be lowercase

@@ -1,2 +1,3 @@
vendor/
.idea
.aider*
Copy link
Member

Choose a reason for hiding this comment

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

nit: can move .gitognore and go.mod changes into a new commit

@@ -162,6 +163,89 @@ func TestBolt4Packet(t *testing.T) {
}
}

func TestTLVPayloadMessagePacket(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: missing docs

pubKey, err := btcec.ParsePubKey(pubKeyBytes)
require.NoError(t, err)

EncryptedRecipientData, err := hex.DecodeString(
Copy link
Member

Choose a reason for hiding this comment

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

should be lowercase

@@ -1,4 +1,4 @@
module github.com/lightningnetwork/lightning-onion
module github.com/gijswijs/lightning-onion
Copy link
Member

Choose a reason for hiding this comment

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

I'm assuming this is changed so you can continue the development in lnd right? If that's the case, I would suggest using go.work instead locally, more details here.

Basically if you have the following dir tree struct,

> tree -L 1
.
├── btcsuite
├── lightning-infra
├── lightning-onion
├── lightning-terminal
├── lnd
...

You can create a go.work file here,

> tree -L 1
.
├── btcsuite
├── go.work
├── itest_logs
├── itest-db
├── lightning-infra
├── lightning-onion
├── lightning-terminal
├── lnd
...

Here's my go.work file,

go 1.23.10

toolchain go1.24.0

use (
	./btcsuite/btcd
	./btcsuite/btcd/btcec
	./btcsuite/btcd/btcutil
	./btcsuite/btcd/btcutil/psbt
	./btcsuite/btcd/chaincfg/chainhash
	./btcsuite/btcwallet
	./btcsuite/btcwallet/wallet/txauthor
	./btcsuite/btcwallet/wallet/txrules
	./btcsuite/btcwallet/wallet/txsizes
	./btcsuite/btcwallet/walletdb
	./btcsuite/btcwallet/wtxmgr
	./falafel
	./go_temp
	./lightning-onion
	./lnd
	./lnd/cert
	./lnd/clock
	./lnd/healthcheck
	./lnd/kvdb
	./lnd/queue
	./lnd/ticker
	./lnd/tor
	./lnd/fn
	./lnd/tlv
	./neutrino
	./lnd/sqldb
)

You may need to run go work sync and go work vendor to make it work. Once done, any local changes in one package (onion, btcd...) will be reflected and updated in lnd.

Finally say you've updated this lightning-onion package and made a few changes in lnd, and want to create a PR in lnd to check the CI, you can create a temp commit in lnd to update the package in lnd's go.mod file,

// note the space instead of an @
replace host.com/someone/pkg => host.com/you/pkg branch

So in this case we put this line in the end,

replace github.com/lightningnetwork/lightning-onion => github.com/gijswijs/lightning-onion onion-messaging

And run go mod tidy should make it work in lnd.

Copy link
Member

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

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

Second round done. My main comments are,

  • the legacy vs empty tlv stream check, and wonder whether it's needed, and if so, can we do it in a higher layer instead. Also wondering if we could drop the support for legacy payload.
  • code format needs to be fixed.
  • need to make it compile, ran linter and got
> golangci-lint run
cmd/main.go:1: : # github.com/gijswijs/lightning-onion/cmd
cmd/main.go:212:25: undefined: sphinx.OnionPacketOption
cmd/main.go:214:40: undefined: sphinx.WithOnionMessage
cmd/main.go:217:14: cannot use ... in call to non-variadic sphinx.NewOnionPacket (typecheck)
package main

I also noticed that we don't have any CI here, maybe a minimal CI that includes jobs like unit test, lint, and check commits would be a good starting point, and it should be easy to add as we can just copy files from lnd. Another PR tho.

require.NoError(t, err)

// Once we have the raw file, we'll unpack it into our
// blindingJsonTestCase struct defined below.
Copy link
Member

Choose a reason for hiding this comment

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

should be onionMessageJsonTestCase

path := make([]*HopInfo, len(h))
currentHop := e
for i, hop := range h {
// The json test vector only specifies the current node
Copy link
Member

Choose a reason for hiding this comment

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

could we rephrase it a bit? Not sure I follow the sentence...

currentHop,
)
nodeID, _ := btcec.ParsePubKey(nodeIDStr)
payload, _ := hex.DecodeString(hop.EncryptedDataTlv)
Copy link
Member

Choose a reason for hiding this comment

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

all these errors should be checked via require.NoError(t, err)

require.NoError(t, err)

// At this point, Dave will give his blinded path to the Sender who will
// then build its own blinded route from itself to Bob via Alice. The
Copy link
Member

Choose a reason for hiding this comment

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

Q: in other words Bob is the introduction node and Alice is the sender?

genData := testCase.Generate.Hops[i]
priv := privKeyFromString(hop.PrivKey)
ephem := pubKeyFromString(
genData.EphemeralPubKey,
Copy link
Member

Choose a reason for hiding this comment

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

nit: can fit in one line

totalPayloadSize := paymentPath.TotalPayloadSize()

var routingInfoLen int
maxRoutingInfoErr := ErrStandardRoutingInfoSizeExceeded
Copy link
Member

Choose a reason for hiding this comment

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

var maxRoutingInfoErr error


// Check whether total payload size doesn't exceed the hard maximum.
if totalPayloadSize > routingInfoLen {
return nil, maxRoutingInfoErr
Copy link
Member

Choose a reason for hiding this comment

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

I would do sth like,

// Assume it's the normal routing.
	routingInfoLen := StandardRoutingInfoSize
	maxRoutingInfoErr := ErrStandardRoutingInfoSizeExceeded

	// Overwrite the values if it's an onion message and the payload
	// exceeds the normal size.
	if cfg.isOnionMessage && totalPayloadSize > StandardRoutingInfoSize {
		routingInfoLen = JumboRoutingInfoSize
		maxRoutingInfoErr = ErrJumboRoutingInfoSizeExceeded
	}

Or even simpler,

	if totalPayloadSize > StandardRoutingInfoSize {
		if !cfg.isOnionMessage {
			return nil, ErrStandardRoutingInfoSizeExceeded
		}

		if totalPayloadSize > JumboRoutingInfoSize {
			return nil, ErrJumboRoutingInfoSizeExceeded
		}
	}

And create a dedicated method, sth like validatePayloadSize to perform this check.


// If this is an onion message, we only expect TLV
// payloads.
if cfg.isOnionMessage && isLegacy {
Copy link
Member

Choose a reason for hiding this comment

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

Think this legacy vs empty tlv kinda increases the complexity a lot - I wonder if we need to check it here, like a higher layer should be able to catch and error out there?

@@ -322,7 +372,8 @@ func rightShift(slice []byte, num int) {
// leaving only the original "filler" bytes produced by this function at the
// last hop. Using this methodology, the size of the field stays constant at
// each hop.
func generateHeaderPadding(key string, path *PaymentPath, sharedSecrets []Hash256) []byte {
func generateHeaderPadding(key string, path *PaymentPath,
sharedSecrets []Hash256, routingInfoLen int) []byte {
Copy link
Member

Choose a reason for hiding this comment

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

nit: a new line below

// header. The MaxPayloadSize bytes at the end are used to
// encrypt/decrypt the fillers when processing the packet of generating
// the HMACs when creating the packet.
numStreamBytes = routingInfoSize * 2
Copy link
Member

Choose a reason for hiding this comment

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

I really like the docs here, and wonder if we could somehow retain it, like make numStreamBytes a method that takes routingInfoLen and returns it, otherwise I see routingInfoLen * 2 and will be wondering why the *2

@yyforyongyu
Copy link
Member

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a 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 comprehensive support for onion messaging, including blinded routes and variable-sized onion packets, which is a significant feature addition. The implementation is well-supported by new tests, including vectors from the BOLT specification.

My review focuses on enhancing the code quality. I've suggested improvements to test robustness by replacing ignored errors with explicit checks, and a simplification of some conditional logic to improve readability and maintainability. These suggestions align with general Go best practices for writing clear and robust code.

Comment on lines +145 to +149
nodeIDStr, _ := hex.DecodeString(
currentHop,
)
nodeID, _ := btcec.ParsePubKey(nodeIDStr)
payload, _ := hex.DecodeString(hop.EncryptedDataTlv)

Choose a reason for hiding this comment

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

medium

While ignoring errors with _ is common in tests for brevity, it can mask issues with test data or underlying functions. Using require.NoError(t, err) from the testify package would make the test more robust by failing immediately with a clear message if an error occurs. This is a good practice for ensuring test quality and reliability.

            nodeIDStr, err := hex.DecodeString(
				currentHop,
			)
			require.NoError(t, err)
			nodeID, err := btcec.ParsePubKey(nodeIDStr)
			require.NoError(t, err)
			payload, err := hex.DecodeString(hop.EncryptedDataTlv)
			require.NoError(t, err)

Comment on lines +238 to +250
var routingInfoLen int
maxRoutingInfoErr := ErrStandardRoutingInfoSizeExceeded
if cfg.isOnionMessage {
switch {
case totalPayloadSize <= StandardRoutingInfoSize:
routingInfoLen = StandardRoutingInfoSize
default:
routingInfoLen = JumboRoutingInfoSize
maxRoutingInfoErr = ErrJumboRoutingInfoSizeExceeded
}
} else {
routingInfoLen = StandardRoutingInfoSize
}

Choose a reason for hiding this comment

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

medium

The logic to determine routingInfoLen can be simplified for better readability and conciseness. The current implementation has some redundancy. By setting a default value and then conditionally overriding it for the jumbo case, the code becomes more direct and easier to follow.

    routingInfoLen := StandardRoutingInfoSize
	maxRoutingInfoErr := ErrStandardRoutingInfoSizeExceeded
	if cfg.isOnionMessage && totalPayloadSize > StandardRoutingInfoSize {
		routingInfoLen = JumboRoutingInfoSize
		maxRoutingInfoErr = ErrJumboRoutingInfoSizeExceeded
	}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In progress
Development

Successfully merging this pull request may close these issues.

3 participants