Skip to content

lnwallet: surface initial commitment key ring for N=0 force-close aux resolution#10804

Open
GeorgeTsagk wants to merge 3 commits into
lightningnetwork:masterfrom
GeorgeTsagk:close-immediately-itest-lnd-7c38
Open

lnwallet: surface initial commitment key ring for N=0 force-close aux resolution#10804
GeorgeTsagk wants to merge 3 commits into
lightningnetwork:masterfrom
GeorgeTsagk:close-immediately-itest-lnd-7c38

Conversation

@GeorgeTsagk
Copy link
Copy Markdown
Collaborator

Description

This PR adds InitialKeyRing to lnwallet.ResolutionReq and populates it only when local commit height is 0 during force-close resolution. It gives aux resolvers the correct initial commitment key context for immediate post-funding (N=0) force closes, while leaving later commit-height force-close behavior unchanged (InitialKeyRing=nil).

At N=0, the commitment output keys are derived from the initial commitment point (pre-channel_ready rotation), so using only the live key ring can misidentify outputs and break the aux components.

@GeorgeTsagk GeorgeTsagk self-assigned this May 13, 2026
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, 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 mechanism to provide auxiliary resolvers with the initial commitment key ring for channels that are force-closed at height 0. By correctly identifying the commitment keys used before the channel_ready rotation, the changes prevent output misidentification and ensure that auxiliary components function correctly during immediate post-funding force-close events.

Highlights

  • Resolution Request Update: Added an InitialKeyRing field to the ResolutionReq struct to support auxiliary resolvers during force-close scenarios.
  • Key Derivation Logic: Implemented initialCommitmentKeyRingFromState to derive the correct commitment keys for channels at height 0, distinguishing them from post-channel_ready states.
  • Force-Close Integration: Updated NewLocalForceCloseSummary to populate the InitialKeyRing when the local commitment height is 0, ensuring aux resolvers have the necessary context.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

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 by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

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 pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the 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 counterproductive. 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.

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.

@github-actions github-actions Bot added the severity-critical Requires expert review - security/consensus critical label May 13, 2026
@github-actions
Copy link
Copy Markdown

🔴 PR Severity: CRITICAL

Automated classification | 2 files | 61 lines changed

🔴 Critical (2 files)
  • lnwallet/aux_resolutions.go - wallet auxiliary resolution logic (lnwallet/*)
  • lnwallet/channel.go - core channel state machine and commitment transaction handling (lnwallet/*)

Analysis

Both changed files reside in the lnwallet package, which is classified as CRITICAL because it encompasses wallet operations, channel funding, signing, and commitment transaction logic — among the most security-sensitive code in the Lightning Network Daemon.

  • lnwallet/channel.go is the central channel state machine; changes here can affect commitment transaction construction, fee handling, and the correctness of channel updates.
  • lnwallet/aux_resolutions.go handles auxiliary (e.g. custom channel) resolution paths, which feed into on-chain fund recovery.

This PR is small in scope (2 files, 61 lines), so no severity bump was triggered. However, the inherent criticality of lnwallet means expert review is required before merging.


To override, add a severity-override-{critical,high,medium,low} label.
<!-- pr-severity-bot -->

Copy link
Copy Markdown

@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 InitialKeyRing field to the ResolutionReq struct and implements initialCommitmentKeyRingFromState to derive the key ring for the initial commitment state at height 0. This allows downstream resolvers to correctly distinguish between initial and subsequent commitment states. The reviewer suggested minor documentation improvements to adhere to the repository's style guide, specifically regarding function comment formatting and the requirement for function documentation.

Comment thread lnwallet/channel.go
return incomingHtlcs, outgoingHtlcs, nil
}

// initialCommitmentKeyRing derives the key ring for the initial commitment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The function comment should start with the function's name (initialCommitmentKeyRingFromState) to adhere to the repository's style guide.

Suggested change
// initialCommitmentKeyRing derives the key ring for the initial commitment
// initialCommitmentKeyRingFromState derives the key ring for the initial commitment
References
  1. Function comments must begin with the function name. (link)

Copy link
Copy Markdown
Collaborator

@jtobin jtobin May 21, 2026

Choose a reason for hiding this comment

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

I think lnd is pretty strict on commits, so the commit making this fix should probably be folded into the appropriate previous one.

Comment thread lnwallet/channel.go Outdated
Comment on lines +672 to +676
func (lc *LightningChannel) initialCommitmentKeyRing(
whoseCommit lntypes.ChannelParty) *CommitmentKeyRing {

return initialCommitmentKeyRingFromState(lc.channelState, whoseCommit)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This function is missing a comment, which is required by the repository's style guide. Please add a comment that describes its purpose.

Suggested change
func (lc *LightningChannel) initialCommitmentKeyRing(
whoseCommit lntypes.ChannelParty) *CommitmentKeyRing {
return initialCommitmentKeyRingFromState(lc.channelState, whoseCommit)
}
// initialCommitmentKeyRing derives the key ring for the initial commitment
// state at height 0 for the channel.
func (lc *LightningChannel) initialCommitmentKeyRing(
whoseCommit lntypes.ChannelParty) *CommitmentKeyRing {
return initialCommitmentKeyRingFromState(lc.channelState, whoseCommit)
}
References
  1. Every function must be commented with its purpose and assumptions. (link)

@GeorgeTsagk GeorgeTsagk force-pushed the close-immediately-itest-lnd-7c38 branch from 077847d to 70cfed1 Compare May 13, 2026 14:14
@GeorgeTsagk GeorgeTsagk requested review from Roasbeef and jtobin May 14, 2026 10:33
@GeorgeTsagk GeorgeTsagk force-pushed the close-immediately-itest-lnd-7c38 branch from 70cfed1 to 94fdcc7 Compare May 15, 2026 11:50
@lightninglabs-deploy
Copy link
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
Collaborator

@jtobin jtobin left a comment

Choose a reason for hiding this comment

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

I'm pretty sure this one can be simplified dramatically, and I think the simplifications will carry over to the associated tapd PR. I commented on a bunch of stuff, but didn't check any of it myself, so it could be wrong. Seems worth following these Opus-generated instructions to check:

  • Drop ResolutionReq.InitialKeyRing; add ResolutionReq.CommitHeight fn.Option[uint64].
  • Delete initialCommitmentKeyRingFromState.
  • In NewLocalForceCloseSummary, set CommitHeight: fn.Some(chanState.LocalCommitment.CommitHeight).
  • In NewUnilateralCloseSummary, set CommitHeight: fn.Some(remoteCommit.CommitHeight).
  • Audit the other ResolutionReq construction sites (lnwallet/channel.go:2311, 2394, 7530, 7766, 7903, 8130) and set CommitHeight from the state-num in scope.

Comment thread lnwallet/channel.go
return incomingHtlcs, outgoingHtlcs, nil
}

// initialCommitmentKeyRing derives the key ring for the initial commitment
Copy link
Copy Markdown
Collaborator

@jtobin jtobin May 21, 2026

Choose a reason for hiding this comment

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

I think lnd is pretty strict on commits, so the commit making this fix should probably be folded into the appropriate previous one.

Comment thread lnwallet/channel.go
// initialCommitmentKeyRing derives the key ring for the initial commitment
// state at height 0. This is distinct from the live key ring after
// channel_ready, which already moved on to the next per-commitment point.
// If the initial commitment point can't be derived, then nil is returned and
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Feels like this should return (nil, error)?

Whilst asking Opus if it agreed (it did), it also happened to point out that the remote and default cases of the switch are now dynamically, i.e. at runtime, unreachable. It is absolutely righteous to handle them anyway, but it recommended wiring up the initial keyring plumbing such that the remote branch was exercised "in case tapd also needs to do the proof-rewrite dance for remote-initiated force closes."

I asked it if tapd does need to do the proof-rewrite dance for remote-initiated force closes, and it seems to think it does. It recommends the following:

  1. In NewUnilateralCloseSummary, populate InitialKeyRing on the ResolutionReq at lnwallet/channel.go:7252 when chanState.RemoteCommitment.CommitHeight == 0 — symmetric to what commit 2 did for the local path. The remote branch of initialCommitmentKeyRingFromState (currently the "dead" one) is exactly what this needs.

  2. The itest (testCustomChannelsImmediateClose) only exercises Alice closing. Add a symmetric scenario where Bob force-closes immediately after funding — that would have caught this asymmetry as a regression.

Seems worth adding that itest case to check if it's actually on to something here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

After looking into the CommitHeight idea I asked Opus what simplifications that might imply. It claims that this function "evaporates entirely:"

The helper exists only to produce a keyring tapd can use as a height-0 signal. Once CommitHeight carries the signal directly and tapd just uses req.KeyRing at height 0 (because KeyRing already is the height-0 keyring there), nothing in lnd needs this helper. It was 40-ish lines including the local/remote switch, the wrapper method (already removed by commit 3), and the doc comment. All gone. That subsumes:

  • The (nil, error) question I raised — there's no helper to fix.
  • The dead remote-branch concern — there's no branch.
  • The mismatch between the helper's "fall back to live key ring" comment
    and commit 2's actual gating — there's no comment.
  • The lntypes.ChannelParty parameter on the helper — gone with the helper.

Worth confirming it's right.

// KeyRing is the key ring for the channel.
KeyRing *CommitmentKeyRing

// InitialKeyRing is the key ring for the initial commitment state at
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This one feels a bit weird since it's a sort of temporary duplication of the KeyRing state. Might be cleaner to pass the commitment height itself, e.g.:

  CommitHeight uint64

and then I think if that's equal to zero you can safely just check the keyring state to get what you need.

(Also, after checking some of the stuff mentioned in other comments, you might actually need fn.Option[uint64] here to distinguish actually-a-zero-commit-height from the default 0 value.)

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

Labels

severity-critical Requires expert review - security/consensus critical

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants