Skip to content

multi: add taproot support to the new RBF close flow #10063

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 11 commits into
base: splice-nonces
Choose a base branch
from

Conversation

Roasbeef
Copy link
Member

@Roasbeef Roasbeef commented Jul 10, 2025

In this PR, we implement comprehensive taproot RBF cooperative channel closing support for LND. The implementation extends the existing RBF cooperative close protocol to support taproot channels using MuSig2 signatures and introduces a JIT (Just-In-Time) nonce pattern for secure and efficient nonce management during RBF iterations.

wire + state machien changes

The implementation introduces taproot-specific wire protocol extensions for the shutdown, closing_complete, and closing_sig messages. The shutdown message now carries closee nonces for taproot channels, while closing_complete uses PartialSigWithNonce structures that bundle partial signatures with next-round nonces. The closing_sig message uses separate PartialSig and NextCloseeNonce fields to optimize nonce delivery timing.

The cooperative close state machine has been extended with taproot channel detection through the IsTaproot() method and comprehensive nonce state management. The implementation maintains separate LocalMusigSession and RemoteMusigSession contexts for handling the asymmetric closer/closee roles in the taproot closing flow.

New signature extraction and validation helpers handle both taproot partial signatures and traditional ECDSA signatures with proper type validation. The partialSigToWireSig, extractTaprootSigAndNonce, and validateAndExtractSigAndNonce functions provide robust signature handling while maintaining backward compatibility.

The state transitions have been updated to properly initialize MuSig2 sessions with appropriate closee nonces, handle signature preparation for both channel types, and manage the complex nonce rotation required for secure RBF iterations. The implementation includes comprehensive error handling for taproot-specific validation requirements.

Fixes #9662

Roasbeef added 5 commits July 9, 2025 20:35
In this commit we, add support for taproot partial signatures with
nonces to the ClosingComplete message. This is the foundation for
taproot RBF cooperative channel closing, implementing the JIT nonce
pattern required for the modern taproot closing flow.

The changes include a new TaprootClosingSigs struct that mirrors the
existing ClosingSigs but uses PartialSigWithNonce for taproot channels.
The decoding and encoding functions are updated to handle both regular
ECDSA signatures and taproot partial signatures. For taproot channels,
the TaprootClosingSigs field is populated while ClosingSigs remains
empty, maintaining backward compatibility.

We also fix a minor typo in the comment for CloserNoClosee field
(clsoee -> closee).
In this commit we, extend the ClosingSig message to support taproot
partial signatures for the RBF cooperative close flow. The ClosingSig
message is sent by the closee in response to a ClosingComplete message.

For taproot channels, we add TaprootPartialSigs which contains partial
signatures without nonces since the remote party already knows our
nonce from the previous ClosingComplete message. We also add a
NextCloseeNonce field for RBF iterations, allowing the closee to
provide a new nonce for the next potential RBF round.

The decoding and encoding functions are updated to handle both regular
signatures and taproot partial signatures, maintaining backward
compatibility with existing non-taproot channels while enabling the
advanced taproot RBF flow.
In this commit we, add support for closee nonces in the Shutdown
message to enable taproot RBF cooperative channel closing. The
ShutdownNonce field allows taproot channels to exchange the initial
nonces required for the MuSig2 signing process during cooperative
closure.

This nonce represents the closee nonce that the remote party will use
when they act as the closer in the RBF flow. The nonce is transmitted
as part of the shutdown flow and is essential for the JIT nonce pattern
used in the modern taproot closing protocol.

The changes maintain backward compatibility as the ShutdownNonce field
is optional and only used for taproot channels that support the
enhanced RBF flow.
In this commit we, update the test message utilities to include the new
taproot signature fields added to ClosingComplete and ClosingSig
messages. This ensures the wire protocol tests properly exercise the
new taproot partial signature functionality.
…ment

In this commit we, add the fundamental infrastructure for taproot RBF
cooperative channel closing. This includes adding taproot channel
detection, MuSig2 session management, and nonce state tracking
throughout the closing state machine.

Key additions include the IsTaproot method on Environment to detect
taproot channels based on the presence of MuSig sessions, and
LocalMusigSession/RemoteMusigSession fields for managing the different
signing contexts. We add NonceState tracking to maintain closee nonces
exchanged during the shutdown phase.

The SendShutdown and ShutdownReceived events are extended to carry
closee nonces for taproot channels, and we add proper error handling
for missing nonces in taproot shutdown messages. These changes provide
the foundation for the taproot-specific state transitions while
maintaining compatibility with existing non-taproot channels.
Copy link

Pull reviewers stats

Stats of the last 30 days for lnd:

User Total reviews Time to review Total comments
guggero
🥇
47
▀▀▀
1d 4h 25m
87
▀▀▀
yyforyongyu
🥈
25
▀▀
22h 50m
43
bhandras
🥉
22
1d 6h 15m
29
ellemouton
14
2d 17h 12m
42
ziggie1984
14
2d 3h 47m
63
▀▀
Roasbeef
8
1d 16h 16m
21
bitromortac
7
1d 6h 40m
13
starius
3
1d 8h 1m
7
ViktorTigerstrom
3
6h 49m
0
Abdulkbk
3
12h 13m
3
saubyk
2
3d 3h 14m
4
MPins
2
2d 19h 55m
4
alexbosworth
1
3d 16h 59m
1
ffranr
1
7d 2h 16m
▀▀
1
gijswijs
1
22m
0
hieblmi
1
10h 12m
0
morehouse
1
7h
0

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 @Roasbeef, 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 LND's cooperative channel closing capabilities by integrating Taproot channel support into the RBF (Replace-by-Fee) flow. It introduces a sophisticated Just-In-Time nonce management system using MuSig2 signatures, along with necessary wire protocol and state machine adjustments, ensuring a secure and efficient closing process for modern Lightning channels while maintaining full backward compatibility.

Highlights

  • Taproot RBF Cooperative Close Support: This PR implements comprehensive support for Taproot channels within the existing RBF (Replace-by-Fee) cooperative channel closing protocol. This includes using MuSig2 signatures and a JIT (Just-In-Time) nonce pattern for secure and efficient nonce management during RBF iterations.
  • Wire Protocol Extensions: The shutdown, closing_complete, and closing_sig messages have been extended with Taproot-specific TLV fields. The shutdown message now carries the sender's closee nonce. closing_complete uses PartialSigWithNonce to bundle partial signatures with next-round nonces, and closing_sig uses separate PartialSig and NextCloseeNonce fields.
  • State Machine Enhancements: The cooperative close state machine has been updated to detect Taproot channels via an IsTaproot() method and manage nonce states. It now maintains separate LocalMusigSession and RemoteMusigSession contexts to handle the asymmetric closer/closee roles in the Taproot closing flow.
  • Signature Handling and Compatibility: New helpers for signature extraction and validation have been introduced to properly handle both Taproot partial signatures and traditional ECDSA signatures. All changes maintain full backward compatibility, activating Taproot functionality only when MuSig2 sessions are present.
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.

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 PR introduces comprehensive support for Taproot channels in the RBF cooperative close flow. The changes are extensive, touching the wire protocol, state machine, and adding MuSig2 signing logic with a JIT nonce pattern. The refactoring of integration tests to cover both taproot and non-taproot channels is well-executed.

My review has identified a critical issue in the nonce handling logic for taproot channels that could lead to signature failures, and a style guide violation regarding line length.

Comment on lines 1318 to 1319
// Extract their closer nonce from PartialSigWithNonce and use it to verify their signature
initRemoteMusigCloseeNonce(env, jitNonce)

Choose a reason for hiding this comment

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

critical

This call to initRemoteMusigCloseeNonce with jitNonce is incorrect and will lead to signature failures for taproot channels.

The jitNonce is extracted from the PartialSigWithNonce field of a closing_complete message. This nonce is the next closee nonce of the sender (the closer), intended for the subsequent RBF iteration. It should not be used to initialize the musig session for the current round of signing/verification.

The musig session should have already been initialized with the nonces exchanged during the shutdown phase. This incorrect re-initialization will corrupt the session state.

This is also in direct contradiction with the documentation in rbf_close.md, which states: "The nonce from PartialSigWithNonce in closing_complete is stored but not immediately used with InitRemoteNonce - it's used when we need to sign as the closee in the next round."

Please remove this line. The jitNonce should be stored for the next round, likely by updating c.NonceState.RemoteCloseeNonce in updateAndValidateCloseTerms for the OfferReceivedEvent case, similar to how it's handled for LocalSigReceived.

Copy link
Member Author

Choose a reason for hiding this comment

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

@gemini-code-assist this was reworked. The new _itended flow is:

1. When we receive their shutdown (they might become closee):

  • They send us their closee nonce in the shutdown message
  • We call InitRemoteNonce on our LocalMusigSession with their closee nonce
  • This prepares our LocalMusigSession for when we act as closer

2. When we send our shutdown (we might become closee):

  • We generate our closee nonce by calling ClosingNonce() on RemoteMusigSession
  • This sets the internal localNonce field in RemoteMusigSession
  • We send this nonce in our shutdown message

3. When we act as closer (sending ClosingComplete):

  • We call ClosingNonce() on LocalMusigSession to generate our JIT closer nonce
  • We sign with this nonce
  • We send both our signature and this JIT nonce in ClosingComplete
  • The nonce in PartialSigWithNonce is the JIT closer nonce we used to sign

4. When we act as closee (receiving ClosingComplete):

  • They send us their JIT closer nonce in ClosingComplete
  • We call InitRemoteNonce on RemoteMusigSession with their JIT closer nonce
  • This completes the RemoteMusigSession so we can verify their signature
  • We respond with ClosingSig containing our partial signature

5. When sending ClosingSig (as closee):

  • We include our partial signature (no nonce needed, they already know it)
  • We include NextCloseeNonce for future RBF iterations

6. When receiving ClosingSig (as closer):

  • We extract their NextCloseeNonce from the message
  • We store it as RemoteCloseeNonce for future RBF iterations
  • If we want to RBF, we'll use this nonce when calling InitRemoteNonce on our `LocalMusigSession

Choose a reason for hiding this comment

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

Thank you for the detailed breakdown of the intended nonce flow, Roasbeef. This clarifies the design significantly.

However, I've noticed a direct contradiction between point 4 of your explanation and the documentation in rbf_close.md that was added in this very pull request.

Your point 4 states:

  1. When we act as closee (receiving ClosingComplete):
    • They send us their JIT closer nonce in ClosingComplete
    • We call InitRemoteNonce on RemoteMusigSession with their JIT closer nonce
    • This completes the RemoteMusigSession so we can verify their signature

Yet, rbf_close.md explicitly states under "Implementation Notes for Nonce Handling":

The nonce from PartialSigWithNonce in closing_complete is stored but not immediately used with InitRemoteNonce - it's used when we need to sign as the closee in the next round.

The code snippet I originally commented on (initRemoteMusigCloseeNonce(env, jitNonce)) aligns with your point 4, meaning it does immediately use the jitNonce from closing_complete to initialize the RemoteMusigSession for the current round's verification. This directly contradicts the documentation.

Could you please clarify which behavior is the correct and intended one? It's crucial for the code and its documentation to be consistent to avoid future misunderstandings and potential bugs. If the code's current behavior (as described in your point 4) is correct, then the documentation should be updated to reflect that jitNonce is used immediately for verification. If the documentation is correct, then the code needs to be adjusted to store jitNonce for the next round instead of using it for the current one.

Copy link
Member Author

Choose a reason for hiding this comment

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

rbf_close.md is out of date, left over from an earlier iteration.

This section:

The nonce from PartialSigWithNonce in closing_complete is stored but not immediately used with InitRemoteNonce - it's used when we need to sign as the closee in the next round.

Should be closing_sig, and just the nonce sent as a distinct TLV field in that message.

Roasbeef added 6 commits July 10, 2025 16:09
In this commit we, implement the complete taproot RBF cooperative close
state machine transitions. This is a comprehensive change that adds all
the necessary components for taproot channel closing support.

The implementation includes several key areas:

First, we add nonce management helpers including initLocalMusigCloseeNonce
and initRemoteMusigCloseeNonce for properly initializing MuSig2 sessions
with the appropriate closee nonces during the RBF flow.

Second, we implement signature extraction and validation helpers including
partialSigToWireSig for converting partial signatures to wire format, and
extractTaprootSigAndNonce, extractSigAndNonce, and validateAndExtractSigAndNonce
for handling both taproot and regular signatures with proper validation.

Third, we add comprehensive signature encoding logic with encodeClosingSignatures
that creates appropriate signature structures for both channel types, and helper
functions like processRemoteTaprootSig, createLocalCloseeSignature, and
createClosingSigMessage for managing the complex taproot signing flow.

Fourth, we extend the shutdown validation logic to require nonces for
taproot channels and update all state transitions to properly handle
nonce exchange, MuSig2 session initialization, and the dual signature
paths for taproot vs non-taproot channels.

Finally, we add signature preparation logic with prepareClosingSignatures
and extraction helpers like extractSigAndNonceFromComplete that handle
the complex musig signature combination required for taproot channels
while maintaining compatibility with existing ECDSA signatures.

The changes maintain backward compatibility with existing non-taproot
channels while enabling the full taproot RBF cooperative close flow
with proper nonce rotation and signature handling.
In this commit we, extend the RBF cooperative close test suite to
support taproot channels. This includes adding schnorr signature
test constants, taproot channel test helpers, and comprehensive
test coverage for the taproot RBF flow.

The changes add localSchnorrSig and remoteSchnorrSig test constants
to mirror the existing ECDSA signatures, and include proper imports
for musig2, chainhash, and lnwallet to support the taproot testing
infrastructure.

The test modifications ensure that both taproot and non-taproot
channels are properly tested throughout the RBF cooperative close
state machine, validating the dual signature handling paths and
nonce management logic introduced in the main implementation.
In this commit we, update the chancloser test utilities and message
mapping functions to properly handle the new taproot-specific fields
in the RBF cooperative close flow.

The changes ensure that test harnesses and message mapping functions
are aware of the taproot signature fields and nonce handling required
for the extended wire protocol support. This maintains test coverage
for both existing non-taproot functionality and the new taproot
capabilities.
In this commit we, extend the integration test suite to cover taproot
RBF cooperative close scenarios. The tests validate the complete
end-to-end flow for taproot channels including nonce exchange during
shutdown, MuSig2 signature handling, and proper RBF iteration with
nonce rotation.

The integration tests ensure that the taproot closing flow works
correctly in a real network environment, exercising both the happy
path and various edge cases specific to taproot channel closing.
This provides comprehensive coverage for the enhanced cooperative
close functionality.
In this commit we, integrate the taproot RBF cooperative close
functionality throughout the LND stack. This includes updating
protocol configuration and peer connection handling to support
the new taproot closing flow.

The changes wire through the taproot channel detection, nonce
exchange during shutdown, and proper handling of the enhanced
wire protocol messages in the peer layer. This completes the
integration of taproot RBF cooperative close functionality,
providing a complete alternate closing path for taproot channels
that leverages MuSig2 signatures and implements proper nonce
rotation for secure RBF scenarios.
In this commit we, update the RBF cooperative close documentation to
comprehensively cover the taproot channel closing flow. The documentation
now explains the JIT nonce pattern, asymmetric signature roles, and the
complete nonce exchange protocol for taproot channels.

Key additions include detailed explanations of how nonces flow through
the RBF process, the distinction between closer and closee roles, and
the specific wire message extensions for PartialSigWithNonce and
NextCloseeNonce fields. The documentation also covers validation
requirements and implementation notes specific to taproot channels.

This documentation provides a complete reference for understanding
and implementing the enhanced taproot RBF cooperative close protocol.
@lightninglabs-deploy
Copy link

@Roasbeef, remember to re-request review from reviewers when ready

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

Successfully merging this pull request may close these issues.

2 participants