Skip to content

Conversation

@ziggie1984
Copy link
Collaborator

@ziggie1984 ziggie1984 commented Nov 21, 2025


Summary

This PR renames broadcastHeight to confirmHeight in commitSweepResolver to accurately reflect what the value represents.

The Problem

The field was named broadcastHeight with a comment stating it's "the height that the original contract was broadcast to the main-chain at." However, this is incorrect - the value is
actually the confirmation height of the commitment transaction (the block height where it was included), not the broadcast height (when it was submitted to the mempool).

Verification

The height passed to newCommitSweepResolver is the confirmation height in both code paths:

  1. Normal operation (commitment tx just confirmed):
    RegisterSpendNtfn fires when funding outpoint is spent (confirmed)
    → SpendDetail.SpendingHeight = confirmation height
    → dispatchLocalForceClose(commitSpend, ...)
    → advanceState(uint32(closeInfo.SpendingHeight), ...)
    → prepContractResolutions(..., height, ...)
    → newCommitSweepResolver(..., confirmHeight, ...)

  2. Restart (channel already pending close):
    ClosingHeight = CloseHeight from ChannelCloseSummary
    → CloseHeight is documented as "the height at which the funding transaction was spent"
    → triggerHeight = c.cfg.ClosingHeight
    → prepContractResolutions(..., triggerHeight, ...)
    → newCommitSweepResolver(..., confirmHeight, ...)

Changes

  • Renamed field broadcastHeight → confirmHeight in commitSweepResolver
  • Updated the comment to accurately describe the value
  • Removed the redundant getCommitTxConfHeight() function which was waiting for confirmation of an already-confirmed transaction
  • Updated test file briefcase_test.go

Backward Compatibility

No backward compatibility issues - the serialization format is a raw uint32 binary value, so only the Go variable name changes, not the wire format.

Future Work

The same misnaming exists in other resolvers that should be addressed in follow-up PRs:

  • anchorResolver
  • htlcTimeoutResolver
  • htlcSuccessResolver
  • htlcOutgoingContestResolver
  • htlcIncomingContestResolver

@ziggie1984 ziggie1984 self-assigned this Nov 21, 2025
@ziggie1984 ziggie1984 requested a review from Roasbeef November 21, 2025 23:12
@ziggie1984 ziggie1984 added contracts back port candidate pr which should be back ported to last major release labels Nov 21, 2025
@ziggie1984 ziggie1984 added this to the v0.20.1 milestone Nov 21, 2025
@ziggie1984 ziggie1984 marked this pull request as ready for review November 21, 2025 23:14
@Roasbeef
Copy link
Member

newCommitSweepResolver has a height passed into it here:

// If this is was an unilateral closure, then we'll also create a
// resolver to sweep our commitment output (but only if it wasn't
// trimmed).
if contractResolutions.CommitResolution != nil {
resolver := newCommitSweepResolver(
*contractResolutions.CommitResolution, height,
c.cfg.ChanPoint, resolverCfg,
)
if chanState != nil {
resolver.SupplementState(chanState)
}
htlcResolvers = append(htlcResolvers, resolver)
}

That height is passed in from the prepContractResolutions function here:

// Now that we know we'll need to act, we'll process all the
// resolvers, then create the structures we need to resolve all
// outstanding contracts.
resolvers, err := c.prepContractResolutions(
contractResolutions, triggerHeight, htlcActions,
)
if err != nil {
log.Errorf("ChannelArbitrator(%v): unable to "+
"resolve contracts: %v", c.cfg.ChanPoint, err)
return StateError, closeTx, err
}

The triggerHeight value is obtained from the stateStep function:

nextState, closeTx, err := c.stateStep(
triggerHeight, trigger, confCommitSet,
)
if err != nil {
log.Errorf("ChannelArbitrator(%v): unable to advance "+
"state: %v", c.cfg.ChanPoint, err)
return priorState, nil, err
}

On a remote/local force close, the height used to advance is the closeHeight :

  • // We'll now advance our state machine until it reaches a terminal
    // state.
    _, _, err = c.advanceState(
    uint32(closeInfo.SpendingHeight),
    localCloseTrigger, &closeInfo.CommitSet,
    )
    if err != nil {
    log.Errorf("Unable to advance state: %v", err)
    }
  • // We'll now advance our state machine until it reaches a terminal
    // state.
    _, _, err = c.advanceState(
    uint32(closeInfo.SpendingHeight),
    remoteCloseTrigger, &closeInfo.CommitSet,
    )
    if err != nil {
    log.Errorf("Unable to advance state: %v", err)
    }

So renaming from broadcastHeight is indeed valid, and we can use that directly instead of potentially needing to a long running rescan to figure out which height the channel closed at.

@Roasbeef
Copy link
Member

Looks like a test needs to be updated:

Details
10 @ 0x48318e 0x4160cb 0x415cf7 0x14892ad 0x48b421
#	0x14892ac	github.com/lightningnetwork/lnd/contractcourt.(*chanArbTestCtx).receiveBlockbeat.func1+0x4c	/home/runner/work/lnd/lnd/contractcourt/channel_arbitrator_test.go:237

3 @ 0x48318e 0x45f917 0x1442951 0x48b421
#	0x1442950	github.com/lightningnetwork/lnd/contractcourt.(*ChannelArbitrator).channelAttendant+0x330	/home/runner/work/lnd/lnd/contractcourt/channel_arbitrator.go:2826

1 @ 0x441ab1 0x481fbd 0x616cd1 0x616ae5 0x61396b 0x14afb7d 0x48b421
#	0x616cd0	runtime/pprof.writeRuntimeProfile+0xb0					/opt/hostedtoolcache/go/1.25.3/x64/src/runtime/pprof/pprof.go:788
#	0x616ae4	runtime/pprof.writeGoroutine+0x44					/opt/hostedtoolcache/go/1.25.3/x64/src/runtime/pprof/pprof.go:747
#	0x61396a	runtime/pprof.(*Profile).WriteTo+0x14a					/opt/hostedtoolcache/go/1.25.3/x64/src/runtime/pprof/pprof.go:371
#	0x14afb7c	github.com/lightningnetwork/lnd/contractcourt.timeout.func1+0x9c	/home/runner/work/lnd/lnd/contractcourt/utils_test.go:21

1 @ 0x48318e 0x4160cb 0x415cf7 0x149a056 0x5c0bca 0x48b421
#	0x149a055	github.com/lightningnetwork/lnd/contractcourt.TestCommitSweepResolverNoDelay+0x275	/home/runner/work/lnd/lnd/contractcourt/commit_sweep_resolver_test.go:202
#	0x5c0bc9	testing.tRunner+0xe9									/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1934

1 @ 0x48318e 0x4160cb 0x415cf7 0x149a17d 0x14486a7 0x14999b8 0x48b421
#	0x149a17c	github.com/lightningnetwork/lnd/contractcourt.TestCommitSweepResolverNoDelay.func1+0x3c			/home/runner/work/lnd/lnd/contractcourt/commit_sweep_resolver_test.go:192
#	0x14486a6	github.com/lightningnetwork/lnd/contractcourt.(*commitSweepResolver).Resolve+0x746			/home/runner/work/lnd/lnd/contractcourt/commit_sweep_resolver.go:196
#	0x14999b7	github.com/lightningnetwork/lnd/contractcourt.(*commitSweepResolverTestContext).resolve.func1+0x57	/home/runner/work/lnd/lnd/contractcourt/commit_sweep_resolver_test.go:89

1 @ 0x48318e 0x4160cb 0x415cf7 0x149a571 0x149ab5b 0x5c0bca 0x48b421
#	0x149a570	github.com/lightningnetwork/lnd/contractcourt.testCommitSweepResolverDelay+0x370	/home/runner/work/lnd/lnd/contractcourt/commit_sweep_resolver_test.go:280
#	0x149ab5a	github.com/lightningnetwork/lnd/contractcourt.TestCommitSweepResolverDelay.func1+0x1a	/home/runner/work/lnd/lnd/contractcourt/commit_sweep_resolver_test.go:356
#	0x5c0bc9	testing.tRunner+0xe9									/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1934

1 @ 0x48318e 0x4160cb 0x415cf7 0x149a93d 0x14486a7 0x14999b8 0x48b421
#	0x149a93c	github.com/lightningnetwork/lnd/contractcourt.testCommitSweepResolverDelay.func1+0x3c			/home/runner/work/lnd/lnd/contractcourt/commit_sweep_resolver_test.go:256
#	0x14486a6	github.com/lightningnetwork/lnd/contractcourt.(*commitSweepResolver).Resolve+0x746			/home/runner/work/lnd/lnd/contractcourt/commit_sweep_resolver.go:196
#	0x14999b7	github.com/lightningnetwork/lnd/contractcourt.(*commitSweepResolverTestContext).resolve.func1+0x57	/home/runner/work/lnd/lnd/contractcourt/commit_sweep_resolver_test.go:89

1 @ 0x48318e 0x4170b3 0x416c12 0x5c10d1 0x5c0c03 0x5c3e34 0x5c27da 0x113cc9e 0x1517386 0x1517381 0x44c8fd 0x48b421
#	0x5c10d0	testing.tRunner.func1+0x450					/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1891
#	0x5c0c02	testing.tRunner+0x122						/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1940
#	0x5c3e33	testing.runTests+0x4b3						/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:2475
#	0x5c27d9	testing.(*M).Run+0x639						/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:2337
#	0x113cc9d	github.com/lightningnetwork/lnd/kvdb.RunTests+0x5d		/home/runner/go/pkg/mod/github.com/lightningnetwork/lnd/[email protected]/test_utils.go:23
#	0x1517385	github.com/lightningnetwork/lnd/contractcourt.TestMain+0xa5	/home/runner/work/lnd/lnd/contractcourt/setup_test.go:10
#	0x1517380	main.main+0xa0							_testmain.go:219
#	0x44c8fc	runtime.main+0x29c						/opt/hostedtoolcache/go/1.25.3/x64/src/runtime/proc.go:285

1 @ 0x48318e 0x4170b3 0x416c12 0x5c1ba5 0x149ab14 0x5c0bca 0x48b421
#	0x5c1ba4	testing.(*T).Run+0x484									/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:2005
#	0x149ab13	github.com/lightningnetwork/lnd/contractcourt.TestCommitSweepResolverDelay+0x153	/home/runner/work/lnd/lnd/contractcourt/commit_sweep_resolver_test.go:355
#	0x5c0bc9	testing.tRunner+0xe9									/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1934

1 @ 0x48318e 0x45f917 0x146e9cb 0x48b421
#	0x146e9ca	github.com/lightningnetwork/lnd/contractcourt.(*UtxoNursery).incubator+0x12a	/home/runner/work/lnd/lnd/contractcourt/utxonursery.go:804

1 @ 0x48318e 0x45f917 0x14afb46 0x48b421
#	0x14afb45	github.com/lightningnetwork/lnd/contractcourt.timeout.func1+0x65	/home/runner/work/lnd/lnd/contractcourt/utils_test.go:19

@Roasbeef
Copy link
Member

Linter is failing:

Error: contractcourt/commit_sweep_resolver_test.go:186:1: the line is 84 characters long, which exceeds the maximum of 80 characters. (ll)
	ctx := newCommitSweepResolverTestContext(t, &res, testCommitSweepConfHeight)
^
Error: contractcourt/commit_sweep_resolver_test.go:248:1: the line is 84 characters long, which exceeds the maximum of 80 characters. (ll)
	ctx := newCommitSweepResolverTestContext(t, &res, testCommitSweepConfHeight)
^
Error: contractcourt/commit_sweep_resolver_test.go:283:1: the line is 83 characters long, which exceeds the maximum of 80 characters. (ll)
	// Expect report to be updated. confirmHeight(99) + maturityDelay(3) = 102.

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 🎎

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 🚢

In addition to the above analysis I've checked a few things,

  • implications on the to_local output on the remote commitment tx: in theory we can just sweep them as long as the force close hits the mempool. However atm RegisterSpendNtfn only notifies when the tx is confirmed, so this is not a concern.
  • advanceState will only start the commit resolver inside prepContractResolutions until the state StateContractClosed is reached, this guarantees that the commit resolver won't be launched until the force close tx is confirmed.
  • we also call c.advanceState inside <-c.resolutionSignal, given that anchor resolvers may be launched for CPFP, this means the commit resolver may receive a trigger height when the anchor resolver is resolved, which should be the confirm height of the closing tx, due to the blockbeat forces the blocks to be handled sequentially in these systems.
  • there's also a trigger height used in <-c.forceCloseReqs, but it should be irrelevant.

// confirmHeight is the block height that the commitment transaction was
// confirmed at. We'll use this value to bound any historical queries to
// the chain for spends/confirmations.
confirmHeight uint32
Copy link
Member

Choose a reason for hiding this comment

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

nit: commitConfirmHeight or forceCloseTxConfirmHeight? at a glance i thought confirmHeight means the confirmed height of this contract

@Roasbeef Roasbeef merged commit 7e70e3d into lightningnetwork:master Nov 24, 2025
36 of 39 checks passed
Roasbeef added a commit that referenced this pull request Nov 24, 2025
@saubyk saubyk linked an issue Dec 4, 2025 that may be closed by this pull request
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

back port candidate pr which should be back ported to last major release contracts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug]: force closed channel stuck in pending but already swept

3 participants