Skip to content

Conversation

GeorgeTsagk
Copy link
Collaborator

Description

Adds a new aux component named AuxChannelNegotiator. The responsibility of this component is to inject custom feature bits on the peer init & reestablish messages. The interface allows for both reading and setting any custom feature bits.

The aux feature bits are treated as a tlv.Blob by LND, and reside within their own TLV type. So any node that does not support/understand aux feature bits will simply drop the TLV and not read it.

@GeorgeTsagk GeorgeTsagk self-assigned this Aug 28, 2025
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 @GeorgeTsagk, 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 a new AuxChannelNegotiator component to LND, enabling auxiliary channel implementations to dynamically inject and process custom feature bits during peer initialization (init) and channel reestablishment (channel_reestablish) messages. These custom feature bits are transmitted as TLV (Type-Length-Value) blobs, ensuring backward compatibility as nodes not supporting them will simply ignore the unknown TLV. This enhancement provides a flexible mechanism for extending channel behavior based on negotiated features.

Highlights

  • New AuxChannelNegotiator Interface: Introduced an interface (lnwallet.AuxChannelNegotiator) that defines methods for getting and processing custom feature bits during init and channel_reestablish messages, allowing for dynamic channel behavior.
  • TLV-Encoded Auxiliary Features: Custom feature bits are now transmitted as tlv.Blob (aliased as lnwire.AuxFeatureBits) within their own TLV type (AuxFeatureBitsTLV), ensuring extensibility and backward compatibility.
  • Integration into Messaging Flow: The AuxChannelNegotiator is integrated into the htlcswitch and peer components to send and receive these auxiliary feature bits during the init and channel_reestablish message exchanges.
  • Configurability: The new negotiator is exposed through AuxComponents in config_builder.go and peer.Config, allowing for its optional configuration and use within the LND system.
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 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. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

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.

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 an AuxChannelNegotiator component to facilitate custom feature bit negotiation during channel initialization and re-establishment. The changes are well-integrated across the relevant parts of the codebase, including configuration, peer management, and the htlcswitch. The new interfaces and message modifications appear correct and align with the described functionality.

I've identified a potential issue with error handling in htlcswitch/link.go where an error from the negotiator might be silently ignored. Additionally, there are some minor opportunities to improve log message formatting for better readability and consistency with the provided style guide. Overall, this is a solid addition.

@GeorgeTsagk
Copy link
Collaborator Author

I extended the AuxChannelNegotiator interface slightly to also include ProcessChannelReady.

Realized that ChannelReestablish requires a reconnection. For the normal lifecycle we would not notify the aux chan negotiator for any newly funded channels, so that's now included by using the ChannelReady message.

// for custom channel negotiation. This is used by aux channel
// implementations to negotiate custom channel behavior during
// channel reestablishment.
AuxFeatures fn.Option[AuxFeatureBits]
Copy link
Member

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 manifest this here. It can just be a part of the ExtraData below, and have client be able to read/write 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.

Alright after looking at the lifecycle of the lnwire.Init / lnwire.ChannelReestablish wire messages here is my summary:

Seems like our convention is that the fields of the wire messages (regardless of where they end up being encoded) are unfolded on the struct level. Then when we dispatch the message via cfg.Peer.SendMessage everything will be encoded into the appropriate record, with ExtraData hosting most of the misc/optional fields.

If we switch the approach to instead use ExtraData everywhere in the hooks:

  • By setting ExtraData prematurely (before the wire message is encoded/sent) then we'll end up having the data overwritten by the Encode method of that message. We could change the way we encode records into ExtraData for the desired wire messages (some flag that maintains existing records?), but would change code that is used by all wire messages.
  • Within the ExtraData namespace we still have to guard against record type conflict. We're currently able to do that because AuxFeatures is defined on the lnd wire message itself, alongside the rest of the fields that get encoded into ExtraData -- making it easier to avoid TLV conflicts.

We can somehow leverage the existing lnwire.MergeAndEncode, but that still requires for a field to be declared for the init/reestablish messages, like we've done elsewhere:

lnd/lnwire/shutdown.go

Lines 40 to 44 in e24e285

// CustomRecords maps TLV types to byte slices, storing arbitrary data
// intended for inclusion in the ExtraData field of the Shutdown
// message.
CustomRecords CustomRecords

The approach above seems to be more generic but might be too big of a placeholder for this scope -- we know we want to encode just a single record for the features here.

@saubyk saubyk added this to lnd v0.20 Sep 10, 2025
@saubyk saubyk moved this to In progress in lnd v0.20 Sep 10, 2025
@saubyk saubyk added this to the v0.20.0 milestone Sep 10, 2025
@saubyk saubyk moved this from In progress to In review in lnd v0.20 Sep 15, 2025
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.

Agree with @Roasbeef here - we should remove the AuxFeatureBitsTLV and only pass custom records between lnd and tapd. AuxFeatureBitsTLV is only meaningful to tapd, but defining and managing it in lnd, it deeply couples the two - whenever a new feature bit tlv is added in tapd, lnd is now forced to make updates.

The existence of the CustomRecords mechanism (TLVType >= 65536) provides a much cleaner path that avoids the problems.

func ParseAndExtractCustomRecords(allExtraData ExtraOpaqueData,

  • No New TLV Type in lnd: We should remove the definition of AuxFeatureBitsTLV from lnd. lnd should not have a hardcoded TLV type that is specific to an auxiliary service.

  • tapd Defines Its Own Types: tapd should define its own TLV types within the custom record range (>= 65536). For example, tapd could define TapdInitDataTLV = 65536 and TapdReestablishDataTLV = 65537.

  • lnd Parses CustomRecords: lnd's wire parsing logic for the init and channel_reestablish messages should be modified to use ParseAndExtractCustomRecords. It would parse out any known, lnd-specific TLV records, and then bundle all remaining records in the custom range into a CustomRecords map.

  • The Interface Passes CustomRecords: The AuxChannelNegotiator interface would be changed to pass this CustomRecords map instead of a specific tlv.Blob.

@GeorgeTsagk
Copy link
Collaborator Author

Agree with @Roasbeef here - we should remove the AuxFeatureBitsTLV and only pass custom records between lnd and tapd. AuxFeatureBitsTLV is only meaningful to tapd, but defining and managing it in lnd, it deeply couples the two - whenever a new feature bit tlv is added in tapd, lnd is now forced to make updates.

Just wanted to mention that the above:

lnd is now forced to make updates

is not true. In the previous version we exposed the tlv type that would be used for the purpose of hosting aux feature bits, and the only thing that LND needed to be aware of was the key under which that blob would be placed. The contents of the blob (the actual aux feature bit vector) is managed and encoded/decoded by tapd. We wouldn't need to unfold its contents or give LND any further awareness as to what it represents, and also we could extend it solely in tapd without needing to update LND.

What we would need to update LND for is the scenario where we need to introduce a new, different custom record to host a different blob.


Latest push removes the AuxFeatures TLV related code and only allows injecting CustomRecords to the init message. Also realized we don't (yet) need custom records on the reestablish message, so for that we're just going to notify the external components, just like we did for channel_ready.

PTAL @Roasbeef @yyforyongyu

msg.CustomRecords = customRecords
}

if len(extraData) > 0 {
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 if statement here and above is just to avoid populating the field with a nil value, which would lead to misallignments in the unit test assertions for the wire message

Copy link
Member

Choose a reason for hiding this comment

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

I don't think that's needed, also the test is not updated. This method is also used in DynCommit and we need to update it similarly to this line,

msg.ExtraData = RandExtraOpaqueData(t, ignoreRecords)

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.

Cool this version is much lighter and cleaner! I think we need to add a unit test to validate whether the new Decode method works as expected. Examples can be found here,

// TestChanUpdate2EncodeDecode tests the encoding and decoding of the
// ChannelUpdate2 message using hardcoded byte slices.
func TestChanUpdate2EncodeDecode(t *testing.T) {

or here,

// TestDynProposeEncodeDecode checks that the Encode and Decode methods work as
// expected.
func TestDynProposeEncodeDecode(t *testing.T) {

msg.CustomRecords = customRecords
}

if len(extraData) > 0 {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think that's needed, also the test is not updated. This method is also used in DynCommit and we need to update it similarly to this line,

msg.ExtraData = RandExtraOpaqueData(t, ignoreRecords)

}

if len(extraData) > 0 {
msg.ExtraData = extraData
Copy link
Member

Choose a reason for hiding this comment

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

This means the msg.ExtraData will always have the raw custom data, when the data read from the wire only contains custom records. I think we should add tests here, similar to https://github.com/lightningnetwork/lnd/blob/master/lnwire/dyn_commit_test.go or https://github.com/lightningnetwork/lnd/blob/master/lnwire/channel_update_2_test.go

There's also #10140 that can be used as example.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a new test in lnwire/init_message_test.go

Also included the msg.ExtraData = RandExtraOpaqueData(t, ignoreRecords) in the existing random test data generator.

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.

Looking good - only a comment about the unit test!

We introduce this new interface with the purpose of injecting and
handling custom records on the init message, and also notifying
external components when receiving the ChannelReady or
ChannelReestablish message.
We now plug-in the aux channel negotiator to the server impl config. We
also provide it to the peer config as that's where it's needed in order
to inject custom records in the appropriate peer messages.
Before calling the new interface we first add the ability for the peer
message itself to encode the new data records.
This is the final step, we actually call the interface and either
provide or retrieve the custom features over the message. We also notify
the aux components when channel reestablish is received.
In order to help external components to query the custom records of a
channel we need to expose the remote peer pub key. We could look-up
custom records based on the funding outpoint, but that relation is
established when receiving the ChannelReady message. The external
components may query the AuxChanState before that message is received,
so let's make sure the peer pub key is also available.
We notify the aux channel negotiator that an established channel is now
ready to use.
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.

LGTM🏁

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

LGTM ❄️

@Roasbeef Roasbeef merged commit df46d18 into lightningnetwork:master Sep 25, 2025
65 of 73 checks passed
@github-project-automation github-project-automation bot moved this from In review to Done in lnd v0.20 Sep 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

4 participants