peer+rpcserver: use new conf scaling for notifications
What changed, and why it matters
This change adjusts how many Bitcoin block confirmations LND waits before telling users a Lightning channel is successfully closed. For cooperative closes, it now scales the wait based on the channel's size (larger channels wait more confirmations) instead of always using 1 confirmation. For force closes, it keeps 1 confirmation for user notifications because a separate internal safety mechanism handles the real security waiting. This is a defensive hardening improvement, not a fix for an active exploit.
Review lnwallet.CloseConfsForCapacity to confirm its scaling curve is appropriate for mainnet channel sizes and reorg risk. Ensure downstream consumers of ChannelCloseUpdate do not treat notification as final settlement. No urgent patch deployment is indicated; treat as routine hardening.
Security signals we found
Confirmation-depth scaling by channel capacity for cooperative closes
Removal of hardcoded 1-confirmation notification for cooperative closes
Explicit 1-confirmation retention for force-close RPC notifications with delegation to channel arbitrator
Defensive hardening against premature close-success reporting
Evidence from the diff
The commit modifies peer/brontide.go and rpcserver.go to pass a configurable numConfs parameter to WaitForChanToClose. Cooperative channel closures now use lnwallet.CloseConfsForCapacity(chanCloser.Channel().Capacity) to determine confirmation depth, replacing the previous hardcoded 1-confirmation notification threshold. Force closes in the RPC server explicitly remain at 1 confirmation for user-facing notifications, with the commit message noting that security-critical confirmation waiting is handled by the channel arbitrator. This reduces the risk of premature user notification for high-value cooperative closes.
Changed components
peer/brontide.gorpcserver.goWaitForChanToClose functionCooperative channel close notification pathForce close RPC notification pathInspect captured patch +26 / −15
diff --git a/peer/brontide.go b/peer/brontide.go
index 560e8d1..466c7d0 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -4465,14 +4465,19 @@ func (p *Brontide) finalizeChanClosure(chanCloser *chancloser.ChanCloser) {
localOut := chanCloser.LocalCloseOutput()
remoteOut := chanCloser.RemoteCloseOutput()
auxOut := chanCloser.AuxOutputs()
- go WaitForChanToClose(
- chanCloser.NegotiationHeight(), notifier, errChan,
- &chanPoint, &closingTxid, closingTx.TxOut[0].PkScript, func() {
- // Respond to the local subsystem which requested the
- // channel closure.
- if closeReq != nil {
- closeReq.Updates <- &ChannelCloseUpdate{
- ClosingTxid: closingTxid[:],
+ // Determine the number of confirmations to wait before
+ // signaling a successful cooperative close, scaled by
+ // channel capacity (see CloseConfsForCapacity).
+ numConfs := lnwallet.CloseConfsForCapacity(chanCloser.Channel().Capacity)
+
+ go WaitForChanToClose(
+ chanCloser.NegotiationHeight(), notifier, errChan,
+ &chanPoint, &closingTxid, closingTx.TxOut[0].PkScript, numConfs, func() {
+ // Respond to the local subsystem which requested the
+ // channel closure.
+ if closeReq != nil {
+ closeReq.Updates <- &ChannelCloseUpdate{
+ ClosingTxid: closingTxid[:],
Success: true,
LocalCloseOutput: localOut,
RemoteCloseOutput: remoteOut,
@@ -4489,16 +4494,15 @@ func (p *Brontide) finalizeChanClosure(chanCloser *chancloser.ChanCloser) {
// finally the callback will be executed. If any error is encountered within
// the function, then it will be sent over the errChan.
func WaitForChanToClose(bestHeight uint32, notifier chainntnfs.ChainNotifier,
- errChan chan error, chanPoint *wire.OutPoint,
- closingTxID *chainhash.Hash, closeScript []byte, cb func()) {
+ errChan chan error, chanPoint *wire.OutPoint,
+ closingTxID *chainhash.Hash, closeScript []byte, numConfs uint32, cb func()) {
peerLog.Infof("Waiting for confirmation of close of ChannelPoint(%v) "+
"with txid: %v", chanPoint, closingTxID)
- // TODO(roasbeef): add param for num needed confs
- confNtfn, err := notifier.RegisterConfirmationsNtfn(
- closingTxID, closeScript, 1, bestHeight,
- )
+ confNtfn, err := notifier.RegisterConfirmationsNtfn(
+ closingTxID, closeScript, numConfs, bestHeight,
+ )
if err != nil {
if errChan != nil {
errChan <- err
diff --git a/rpcserver.go b/rpcserver.go
index be4d056..e5d59a3 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -2845,9 +2845,16 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
errChan = make(chan error, 1)
notifier := r.server.cc.ChainNotifier
+
+ // For force closes, we notify the RPC client immediately after
+ // 1 confirmation. The actual security-critical confirmation
+ // waiting is handled by the channel arbitrator.
+ numConfs := uint32(1)
+
go peer.WaitForChanToClose(
uint32(bestHeight), notifier, errChan, chanPoint,
- &closingTxid, closingTx.TxOut[0].PkScript, func() {
+ &closingTxid, closingTx.TxOut[0].PkScript, numConfs,
+ func() {
// Respond to the local subsystem which
// requested the channel closure.
updateChan <- &peer.ChannelCloseUpdate{
Why this scored 32/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.