contractcourt: track close confirmation height in chain watcher
What changed, and why it matters
This commit improves how LND records the block height at which a channel-closing transaction is confirmed. It stores the confirmation height when a close is detected and clears it if a blockchain reorganization removes the transaction. This is a bookkeeping/UI improvement rather than a fix for a known security vulnerability.
Treat as a routine reliability/observability improvement. No urgent security action required. Reviewers may want to confirm that MarkCloseConfirmationHeight and ResetCloseConfirmationHeight handle database errors safely and that the height is re-recorded correctly after a reorg.
Security signals we found
Reorg handling added for close confirmation height
State consistency improvement between chain watcher and channel state
No evidence of vulnerability disclosure or exploitability in diff
Evidence from the diff
The change adds calls to MarkCloseConfirmationHeight and ResetCloseConfirmationHeight inside the chain watcher. When a spend is detected, the spending height is persisted so remaining confirmations can be reported. When a reorg invalidates the spend, the height is reset to zero. The patch is defensive and observability-focused; it does not alter consensus rules, signature validation, or transaction handling.
Changed components
contractcourt/chain_watcher.gochannel state close confirmation height trackingInspect captured patch +21 / −1
diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go
index c802fc4..e45bb3d 100644
--- a/contractcourt/chain_watcher.go
+++ b/contractcourt/chain_watcher.go
@@ -760,6 +760,18 @@ func (c *chainWatcher) processDetectedSpend(
numConfs := c.requiredConfsForSpend()
txid := spend.SpenderTxHash
+ // Record the close confirmation height. This is the height at which
+ // the closing tx was first included in a block. We store this so we
+ // can report the remaining confirmations to the user.
+ err := c.cfg.chanState.MarkCloseConfirmationHeight(
+ fn.Some(uint32(spend.SpendingHeight)),
+ )
+ if err != nil {
+ log.Warnf("ChannelPoint(%v): unable to mark close "+
+ "confirmation height: %v",
+ c.cfg.chanState.FundingOutpoint, err)
+ }
+
newConfNtfn, err := c.cfg.notifier.RegisterConfirmationsNtfn(
txid, spend.SpendingTx.TxOut[0].PkScript, numConfs,
uint32(spend.SpendingHeight),
@@ -931,8 +943,16 @@ func (c *chainWatcher) closeObserver() {
confNtfn = nil
pendingSpend = nil
+ // Reset the close confirmation height since the spend
+ // was reorged out.
+ err := c.cfg.chanState.ResetCloseConfirmationHeight()
+ if err != nil {
+ log.Warnf("ChannelPoint(%v): unable to reset "+
+ "close confirmation height: %v",
+ c.cfg.chanState.FundingOutpoint, err)
+ }
+
spendNtfn.Cancel()
- var err error
spendNtfn, err = registerForSpend()
if err != nil {
log.Errorf("Unable to re-register for "+
Why this scored 22/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.