From 074676397d8a298aadb8d815b74071259a615588 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Wed, 13 May 2026 12:24:21 +0000 Subject: [PATCH 1/3] lnwallet: surface initial commitment key ring for force close --- lnwallet/aux_resolutions.go | 6 +++++ lnwallet/channel.go | 50 +++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/lnwallet/aux_resolutions.go b/lnwallet/aux_resolutions.go index 14802c57c7b..392e80ce846 100644 --- a/lnwallet/aux_resolutions.go +++ b/lnwallet/aux_resolutions.go @@ -94,6 +94,12 @@ type ResolutionReq struct { // KeyRing is the key ring for the channel. KeyRing *CommitmentKeyRing + // InitialKeyRing is the key ring for the initial commitment state at + // height 0. This lets downstream resolvers distinguish "just opened" + // commitment outputs from later states that use the post-channel_ready + // commitment point. + InitialKeyRing *CommitmentKeyRing + // CsvDelay is the CSV delay for the local output for this commitment. CsvDelay uint32 diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 78ca895655a..c89595d99d0 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -633,6 +633,49 @@ func (lc *LightningChannel) extractPayDescs(feeRate chainfee.SatPerKWeight, return incomingHtlcs, outgoingHtlcs, nil } +// 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 +// the caller should fall back to the live key ring. +func initialCommitmentKeyRingFromState(chanState *channeldb.OpenChannel, + whoseCommit lntypes.ChannelParty) *CommitmentKeyRing { + + switch { + case whoseCommit.IsLocal(): + revocation, err := chanState.RevocationProducer.AtIndex(0) + if err != nil { + return nil + } + + commitPoint := input.ComputeCommitmentPoint(revocation[:]) + return DeriveCommitmentKeys( + commitPoint, lntypes.Local, chanState.ChanType, + &chanState.LocalChanCfg, &chanState.RemoteChanCfg, + ) + + case whoseCommit.IsRemote(): + if chanState.RemoteCurrentRevocation == nil { + return nil + } + + return DeriveCommitmentKeys( + chanState.RemoteCurrentRevocation, lntypes.Remote, + chanState.ChanType, &chanState.LocalChanCfg, + &chanState.RemoteChanCfg, + ) + + default: + return nil + } +} + +func (lc *LightningChannel) initialCommitmentKeyRing( + whoseCommit lntypes.ChannelParty) *CommitmentKeyRing { + + return initialCommitmentKeyRingFromState(lc.channelState, whoseCommit) +} + // diskCommitToMemCommit converts the on-disk commitment format to our // in-memory commitment format which is needed in order to properly resume // channel operations after a restart. @@ -8539,8 +8582,11 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, ContractPoint: commitResolution.SelfOutPoint, SignDesc: commitResolution.SelfOutputSignDesc, KeyRing: keyRing, - CsvDelay: csvTimeout, - CommitFee: chanState.LocalCommitment.CommitFee, + InitialKeyRing: initialCommitmentKeyRingFromState( + chanState, lntypes.Local, + ), + CsvDelay: csvTimeout, + CommitFee: chanState.LocalCommitment.CommitFee, }) }, ) From 7dbf3b7208f809909e431825f2af8094d2a759fe Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Wed, 13 May 2026 12:28:12 +0000 Subject: [PATCH 2/3] lnwallet: only set initial key ring at commit height zero --- lnwallet/channel.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index c89595d99d0..3281751fa5d 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -8564,6 +8564,13 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, // At this point, we'll check to see if we need any extra // resolution data for this output. + var initialKeyRing *CommitmentKeyRing + if chanState.LocalCommitment.CommitHeight == 0 { + initialKeyRing = initialCommitmentKeyRingFromState( + chanState, lntypes.Local, + ) + } + resolveBlob := fn.MapOptionZ( auxResolver, func(a AuxContractResolver) fn.Result[tlv.Blob] { @@ -8582,9 +8589,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, ContractPoint: commitResolution.SelfOutPoint, SignDesc: commitResolution.SelfOutputSignDesc, KeyRing: keyRing, - InitialKeyRing: initialCommitmentKeyRingFromState( - chanState, lntypes.Local, - ), + InitialKeyRing: initialKeyRing, CsvDelay: csvTimeout, CommitFee: chanState.LocalCommitment.CommitFee, }) From 94fdcc7232c55d5fddfe2be9866fabec73d4ea76 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Wed, 13 May 2026 14:09:58 +0000 Subject: [PATCH 3/3] lnwallet: fix lint in initial key ring plumbing --- lnwallet/channel.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 3281751fa5d..6c0e31ccfa6 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -649,6 +649,7 @@ func initialCommitmentKeyRingFromState(chanState *channeldb.OpenChannel, } commitPoint := input.ComputeCommitmentPoint(revocation[:]) + return DeriveCommitmentKeys( commitPoint, lntypes.Local, chanState.ChanType, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, @@ -670,12 +671,6 @@ func initialCommitmentKeyRingFromState(chanState *channeldb.OpenChannel, } } -func (lc *LightningChannel) initialCommitmentKeyRing( - whoseCommit lntypes.ChannelParty) *CommitmentKeyRing { - - return initialCommitmentKeyRingFromState(lc.channelState, whoseCommit) -} - // diskCommitToMemCommit converts the on-disk commitment format to our // in-memory commitment format which is needed in order to properly resume // channel operations after a restart. @@ -8590,8 +8585,8 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, SignDesc: commitResolution.SelfOutputSignDesc, KeyRing: keyRing, InitialKeyRing: initialKeyRing, - CsvDelay: csvTimeout, - CommitFee: chanState.LocalCommitment.CommitFee, + CsvDelay: csvTimeout, + CommitFee: chanState.LocalCommitment.CommitFee, }) }, )