• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 15736109134

18 Jun 2025 02:46PM UTC coverage: 58.197% (-10.1%) from 68.248%
15736109134

Pull #9752

github

web-flow
Merge d2634a68c into 31c74f20f
Pull Request #9752: routerrpc: reject payment to invoice that don't have payment secret or blinded paths

6 of 13 new or added lines in 2 files covered. (46.15%)

28331 existing lines in 455 files now uncovered.

97860 of 168153 relevant lines covered (58.2%)

1.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.4
/lnwallet/chancloser/chancloser.go
1
package chancloser
2

3
import (
4
        "bytes"
5
        "fmt"
6

7
        "github.com/btcsuite/btcd/btcec/v2"
8
        "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
9
        "github.com/btcsuite/btcd/btcutil"
10
        "github.com/btcsuite/btcd/chaincfg"
11
        "github.com/btcsuite/btcd/txscript"
12
        "github.com/btcsuite/btcd/wire"
13
        "github.com/davecgh/go-spew/spew"
14
        "github.com/lightningnetwork/lnd/channeldb"
15
        "github.com/lightningnetwork/lnd/fn/v2"
16
        "github.com/lightningnetwork/lnd/htlcswitch"
17
        "github.com/lightningnetwork/lnd/input"
18
        "github.com/lightningnetwork/lnd/labels"
19
        "github.com/lightningnetwork/lnd/lntypes"
20
        "github.com/lightningnetwork/lnd/lnutils"
21
        "github.com/lightningnetwork/lnd/lnwallet"
22
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
23
        "github.com/lightningnetwork/lnd/lnwire"
24
)
25

26
var (
27
        // ErrChanAlreadyClosing is returned when a channel shutdown is
28
        // attempted more than once.
29
        ErrChanAlreadyClosing = fmt.Errorf("channel shutdown already initiated")
30

31
        // ErrChanCloseNotFinished is returned when a caller attempts to access
32
        // a field or function that is contingent on the channel closure
33
        // negotiation already being completed.
34
        ErrChanCloseNotFinished = fmt.Errorf("close negotiation not finished")
35

36
        // ErrInvalidState is returned when the closing state machine receives a
37
        // message while it is in an unknown state.
38
        ErrInvalidState = fmt.Errorf("invalid state")
39

40
        // ErrUpfrontShutdownScriptMismatch is returned when a peer or end user
41
        // provides a cooperative close script which does not match the upfront
42
        // shutdown script previously set for that party.
43
        ErrUpfrontShutdownScriptMismatch = fmt.Errorf("shutdown script does not " +
44
                "match upfront shutdown script")
45

46
        // ErrProposalExceedsMaxFee is returned when as the initiator, the
47
        // latest fee proposal sent by the responder exceed our max fee.
48
        // responder.
49
        ErrProposalExceedsMaxFee = fmt.Errorf("latest fee proposal exceeds " +
50
                "max fee")
51

52
        // ErrInvalidShutdownScript is returned when we receive an address from
53
        // a peer that isn't either a p2wsh or p2tr address.
54
        ErrInvalidShutdownScript = fmt.Errorf("invalid shutdown script")
55

56
        // errNoShutdownNonce is returned when a shutdown message is received
57
        // w/o a nonce for a taproot channel.
58
        errNoShutdownNonce = fmt.Errorf("shutdown nonce not populated")
59
)
60

61
// closeState represents all the possible states the channel closer state
62
// machine can be in. Each message will either advance to the next state, or
63
// remain at the current state. Once the state machine reaches a state of
64
// closeFinished, then negotiation is over.
65
type closeState uint8
66

67
const (
68
        // closeIdle is the initial starting state. In this state, the state
69
        // machine has been instantiated, but no state transitions have been
70
        // attempted. If a state machine receives a message while in this state,
71
        // then it is the responder to an initiated cooperative channel closure.
72
        closeIdle closeState = iota
73

74
        // closeShutdownInitiated is the state that's transitioned to once the
75
        // initiator of a closing workflow sends the shutdown message. At this
76
        // point, they're waiting for the remote party to respond with their own
77
        // shutdown message. After which, they'll both enter the fee negotiation
78
        // phase.
79
        closeShutdownInitiated
80

81
        // closeAwaitingFlush is the state that's transitioned to once both
82
        // Shutdown messages have been exchanged but we are waiting for the
83
        // HTLCs to clear out of the channel.
84
        closeAwaitingFlush
85

86
        // closeFeeNegotiation is the third, and most persistent state. Both
87
        // parties enter this state after they've sent and received a shutdown
88
        // message. During this phase, both sides will send monotonically
89
        // increasing fee requests until one side accepts the last fee rate
90
        // offered by the other party. In this case, the party will broadcast
91
        // the closing transaction, and send the accepted fee to the remote
92
        // party. This then causes a shift into the closeFinished state.
93
        closeFeeNegotiation
94

95
        // closeFinished is the final state of the state machine. In this state,
96
        // a side has accepted a fee offer and has broadcast the valid closing
97
        // transaction to the network. During this phase, the closing
98
        // transaction becomes available for examination.
99
        closeFinished
100
)
101

102
const (
103
        // defaultMaxFeeMultiplier is a multiplier we'll apply to the ideal fee
104
        // of the initiator, to decide when the negotiated fee is too high. By
105
        // default, we want to bail out if we attempt to negotiate a fee that's
106
        // 3x higher than our max fee.
107
        defaultMaxFeeMultiplier = 3
108
)
109

110
// DeliveryAddrWithKey wraps a normal delivery addr, but also includes the
111
// internal key for the delivery addr if known.
112
type DeliveryAddrWithKey struct {
113
        // DeliveryAddress is the raw, serialized pkScript of the delivery
114
        // address.
115
        lnwire.DeliveryAddress
116

117
        // InternalKey is the Taproot internal key of the delivery address, if
118
        // the address is a P2TR output.
119
        InternalKey fn.Option[btcec.PublicKey]
120
}
121

122
// ChanCloseCfg holds all the items that a ChanCloser requires to carry out its
123
// duties.
124
type ChanCloseCfg struct {
125
        // Channel is the channel that should be closed.
126
        Channel Channel
127

128
        // MusigSession is used to handle generating musig2 nonces, and also
129
        // creating the proper set of closing options for taproot channels.
130
        MusigSession MusigSession
131

132
        // BroadcastTx broadcasts the passed transaction to the network.
133
        BroadcastTx func(*wire.MsgTx, string) error
134

135
        // DisableChannel disables a channel, resulting in it not being able to
136
        // forward payments.
137
        DisableChannel func(wire.OutPoint) error
138

139
        // Disconnect will disconnect from the remote peer in this close.
140
        Disconnect func() error
141

142
        // MaxFee, is non-zero represents the highest fee that the initiator is
143
        // willing to pay to close the channel.
144
        MaxFee chainfee.SatPerKWeight
145

146
        // ChainParams holds the parameters of the chain that we're active on.
147
        ChainParams *chaincfg.Params
148

149
        // Quit is a channel that should be sent upon in the occasion the state
150
        // machine should cease all progress and shutdown.
151
        Quit chan struct{}
152

153
        // FeeEstimator is used to estimate the absolute starting co-op close
154
        // fee.
155
        FeeEstimator CoopFeeEstimator
156

157
        // AuxCloser is an optional interface that can be used to modify the
158
        // way the co-op close process proceeds.
159
        AuxCloser fn.Option[AuxChanCloser]
160
}
161

162
// ChanCloser is a state machine that handles the cooperative channel closure
163
// procedure. This includes shutting down a channel, marking it ineligible for
164
// routing HTLC's, negotiating fees with the remote party, and finally
165
// broadcasting the fully signed closure transaction to the network.
166
type ChanCloser struct {
167
        // state is the current state of the state machine.
168
        state closeState
169

170
        // cfg holds the configuration for this ChanCloser instance.
171
        cfg ChanCloseCfg
172

173
        // chanPoint is the full channel point of the target channel.
174
        chanPoint wire.OutPoint
175

176
        // cid is the full channel ID of the target channel.
177
        cid lnwire.ChannelID
178

179
        // negotiationHeight is the height that the fee negotiation begun at.
180
        negotiationHeight uint32
181

182
        // closingTx is the final, fully signed closing transaction. This will
183
        // only be populated once the state machine shifts to the closeFinished
184
        // state.
185
        closingTx *wire.MsgTx
186

187
        // idealFeeSat is the ideal fee that the state machine should initially
188
        // offer when starting negotiation. This will be used as a baseline.
189
        idealFeeSat btcutil.Amount
190

191
        // maxFee is the highest fee the initiator is willing to pay to close
192
        // out the channel. This is either a use specified value, or a default
193
        // multiplier based of the initial starting ideal fee.
194
        maxFee btcutil.Amount
195

196
        // idealFeeRate is our ideal fee rate.
197
        idealFeeRate chainfee.SatPerKWeight
198

199
        // lastFeeProposal is the last fee that we proposed to the remote party.
200
        // We'll use this as a pivot point to ratchet our next offer up, down,
201
        // or simply accept the remote party's prior offer.
202
        lastFeeProposal btcutil.Amount
203

204
        // priorFeeOffers is a map that keeps track of all the proposed fees
205
        // that we've offered during the fee negotiation. We use this map to cut
206
        // the negotiation early if the remote party ever sends an offer that
207
        // we've sent in the past. Once negotiation terminates, we can extract
208
        // the prior signature of our accepted offer from this map.
209
        //
210
        // TODO(roasbeef): need to ensure if they broadcast w/ any of our prior
211
        // sigs, we are aware of
212
        priorFeeOffers map[btcutil.Amount]*lnwire.ClosingSigned
213

214
        // closeReq is the initial closing request. This will only be populated
215
        // if we're the initiator of this closing negotiation.
216
        //
217
        // TODO(roasbeef): abstract away
218
        closeReq *htlcswitch.ChanClose
219

220
        // localDeliveryScript is the script that we'll send our settled channel
221
        // funds to.
222
        localDeliveryScript []byte
223

224
        // localInternalKey is the local delivery address Taproot internal key,
225
        // if the local delivery script is a P2TR output.
226
        localInternalKey fn.Option[btcec.PublicKey]
227

228
        // remoteDeliveryScript is the script that we'll send the remote party's
229
        // settled channel funds to.
230
        remoteDeliveryScript []byte
231

232
        // closer is ChannelParty who initiated the coop close
233
        closer lntypes.ChannelParty
234

235
        // cachedClosingSigned is a cached copy of a received ClosingSigned that
236
        // we use to handle a specific race condition caused by the independent
237
        // message processing queues.
238
        cachedClosingSigned fn.Option[lnwire.ClosingSigned]
239

240
        // localCloseOutput is the local output on the closing transaction that
241
        // the local party should be paid to. This will only be populated if the
242
        // local balance isn't dust.
243
        localCloseOutput fn.Option[CloseOutput]
244

245
        // remoteCloseOutput is the remote output on the closing transaction
246
        // that the remote party should be paid to. This will only be populated
247
        // if the remote balance isn't dust.
248
        remoteCloseOutput fn.Option[CloseOutput]
249

250
        // auxOutputs are the optional additional outputs that might be added to
251
        // the closing transaction.
252
        auxOutputs fn.Option[AuxCloseOutputs]
253
}
254

255
// calcCoopCloseFee computes an "ideal" absolute co-op close fee given the
256
// delivery scripts of both parties and our ideal fee rate.
257
func calcCoopCloseFee(chanType channeldb.ChannelType,
258
        localOutput, remoteOutput *wire.TxOut,
259
        idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
3✔
260

3✔
261
        var weightEstimator input.TxWeightEstimator
3✔
262

3✔
263
        if chanType.IsTaproot() {
3✔
264
                weightEstimator.AddWitnessInput(
×
265
                        input.TaprootSignatureWitnessSize,
×
266
                )
×
267
        } else {
3✔
268
                weightEstimator.AddWitnessInput(input.MultiSigWitnessSize)
3✔
269
        }
3✔
270

271
        // One of these outputs might be dust, so we'll skip adding it to our
272
        // mock transaction, so the fees are more accurate.
273
        if localOutput != nil {
6✔
274
                weightEstimator.AddTxOutput(localOutput)
3✔
275
        }
3✔
276
        if remoteOutput != nil {
6✔
277
                weightEstimator.AddTxOutput(remoteOutput)
3✔
278
        }
3✔
279

280
        totalWeight := weightEstimator.Weight()
3✔
281

3✔
282
        return idealFeeRate.FeeForWeight(totalWeight)
3✔
283
}
284

285
// SimpleCoopFeeEstimator is the default co-op close fee estimator. It assumes
286
// a normal segwit v0 channel, and that no outputs on the closing transaction
287
// are dust.
288
type SimpleCoopFeeEstimator struct {
289
}
290

291
// EstimateFee estimates an _absolute_ fee for a co-op close transaction given
292
// the local+remote tx outs (for the co-op close transaction), channel type,
293
// and ideal fee rate.
294
func (d *SimpleCoopFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
295
        localTxOut, remoteTxOut *wire.TxOut,
296
        idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
3✔
297

3✔
298
        return calcCoopCloseFee(chanType, localTxOut, remoteTxOut, idealFeeRate)
3✔
299
}
3✔
300

301
// NewChanCloser creates a new instance of the channel closure given the passed
302
// configuration, and delivery+fee preference. The final argument should only
303
// be populated iff, we're the initiator of this closing request.
304
func NewChanCloser(cfg ChanCloseCfg, deliveryScript DeliveryAddrWithKey,
305
        idealFeePerKw chainfee.SatPerKWeight, negotiationHeight uint32,
306
        closeReq *htlcswitch.ChanClose,
307
        closer lntypes.ChannelParty) *ChanCloser {
3✔
308

3✔
309
        chanPoint := cfg.Channel.ChannelPoint()
3✔
310
        cid := lnwire.NewChanIDFromOutPoint(chanPoint)
3✔
311
        return &ChanCloser{
3✔
312
                closeReq:            closeReq,
3✔
313
                state:               closeIdle,
3✔
314
                chanPoint:           chanPoint,
3✔
315
                cid:                 cid,
3✔
316
                cfg:                 cfg,
3✔
317
                negotiationHeight:   negotiationHeight,
3✔
318
                idealFeeRate:        idealFeePerKw,
3✔
319
                localInternalKey:    deliveryScript.InternalKey,
3✔
320
                localDeliveryScript: deliveryScript.DeliveryAddress,
3✔
321
                priorFeeOffers: make(
3✔
322
                        map[btcutil.Amount]*lnwire.ClosingSigned,
3✔
323
                ),
3✔
324
                closer: closer,
3✔
325
        }
3✔
326
}
3✔
327

328
// initFeeBaseline computes our ideal fee rate, and also the largest fee we'll
329
// accept given information about the delivery script of the remote party.
330
func (c *ChanCloser) initFeeBaseline() {
3✔
331
        // Depending on if a balance ends up being dust or not, we'll pass a
3✔
332
        // nil TxOut into the EstimateFee call which can handle it.
3✔
333
        var localTxOut, remoteTxOut *wire.TxOut
3✔
334
        if isDust, _ := c.cfg.Channel.LocalBalanceDust(); !isDust {
6✔
335
                localTxOut = &wire.TxOut{
3✔
336
                        PkScript: c.localDeliveryScript,
3✔
337
                        Value:    0,
3✔
338
                }
3✔
339
        }
3✔
340
        if isDust, _ := c.cfg.Channel.RemoteBalanceDust(); !isDust {
6✔
341
                remoteTxOut = &wire.TxOut{
3✔
342
                        PkScript: c.remoteDeliveryScript,
3✔
343
                        Value:    0,
3✔
344
                }
3✔
345
        }
3✔
346

347
        // Given the target fee-per-kw, we'll compute what our ideal _total_
348
        // fee will be starting at for this fee negotiation.
349
        c.idealFeeSat = c.cfg.FeeEstimator.EstimateFee(
3✔
350
                0, localTxOut, remoteTxOut, c.idealFeeRate,
3✔
351
        )
3✔
352

3✔
353
        // When we're the initiator, we'll want to also factor in the highest
3✔
354
        // fee we want to pay. This'll either be 3x the ideal fee, or the
3✔
355
        // specified explicit max fee.
3✔
356
        c.maxFee = c.idealFeeSat * defaultMaxFeeMultiplier
3✔
357
        if c.cfg.MaxFee > 0 {
6✔
358
                c.maxFee = c.cfg.FeeEstimator.EstimateFee(
3✔
359
                        0, localTxOut, remoteTxOut, c.cfg.MaxFee,
3✔
360
                )
3✔
361
        }
3✔
362

363
        // TODO(ziggie): Make sure the ideal fee is not higher than the max fee.
364
        // Either error out or cap the ideal fee at the max fee.
365

366
        chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) "+
3✔
367
                "is: %v sat (max_fee=%v sat)", c.cfg.Channel.ChannelPoint(),
3✔
368
                int64(c.idealFeeSat), int64(c.maxFee))
3✔
369
}
370

371
// initChanShutdown begins the shutdown process by un-registering the channel,
372
// and creating a valid shutdown message to our target delivery address.
373
func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) {
3✔
374
        // With both items constructed we'll now send the shutdown message for
3✔
375
        // this particular channel, advertising a shutdown request to our
3✔
376
        // desired closing script.
3✔
377
        shutdown := lnwire.NewShutdown(c.cid, c.localDeliveryScript)
3✔
378

3✔
379
        // At this point, we'll check to see if we have any custom records to
3✔
380
        // add to the shutdown message.
3✔
381
        err := fn.MapOptionZ(c.cfg.AuxCloser, func(a AuxChanCloser) error {
3✔
382
                shutdownCustomRecords, err := a.ShutdownBlob(AuxShutdownReq{
×
383
                        ChanPoint:   c.chanPoint,
×
384
                        ShortChanID: c.cfg.Channel.ShortChanID(),
×
385
                        Initiator:   c.cfg.Channel.IsInitiator(),
×
386
                        InternalKey: c.localInternalKey,
×
387
                        CommitBlob:  c.cfg.Channel.LocalCommitmentBlob(),
×
388
                        FundingBlob: c.cfg.Channel.FundingBlob(),
×
389
                })
×
390
                if err != nil {
×
391
                        return err
×
392
                }
×
393

394
                shutdownCustomRecords.WhenSome(func(cr lnwire.CustomRecords) {
×
395
                        shutdown.CustomRecords = cr
×
396
                })
×
397

398
                return nil
×
399
        })
400
        if err != nil {
3✔
401
                return nil, err
×
402
        }
×
403

404
        // If this is a taproot channel, then we'll need to also generate a
405
        // nonce that'll be used sign the co-op close transaction offer.
406
        if c.cfg.Channel.ChanType().IsTaproot() {
6✔
407
                firstClosingNonce, err := c.cfg.MusigSession.ClosingNonce()
3✔
408
                if err != nil {
3✔
409
                        return nil, err
×
410
                }
×
411

412
                shutdown.ShutdownNonce = lnwire.SomeShutdownNonce(
3✔
413
                        firstClosingNonce.PubNonce,
3✔
414
                )
3✔
415

3✔
416
                chancloserLog.Infof("Initiating shutdown w/ nonce: %v",
3✔
417
                        spew.Sdump(firstClosingNonce.PubNonce))
3✔
418
        }
419

420
        // Before closing, we'll attempt to send a disable update for the
421
        // channel.  We do so before closing the channel as otherwise the
422
        // current edge policy won't be retrievable from the graph.
423
        if err := c.cfg.DisableChannel(c.chanPoint); err != nil {
3✔
UNCOV
424
                chancloserLog.Warnf("Unable to disable channel %v on close: %v",
×
UNCOV
425
                        c.chanPoint, err)
×
UNCOV
426
        }
×
427

428
        chancloserLog.Infof("ChannelPoint(%v): sending shutdown message",
3✔
429
                c.chanPoint)
3✔
430

3✔
431
        // At this point, we persist any relevant info regarding the Shutdown
3✔
432
        // message we are about to send in order to ensure that if a
3✔
433
        // re-establish occurs then we will re-send the same Shutdown message.
3✔
434
        shutdownInfo := channeldb.NewShutdownInfo(
3✔
435
                c.localDeliveryScript, c.closer.IsLocal(),
3✔
436
        )
3✔
437
        err = c.cfg.Channel.MarkShutdownSent(shutdownInfo)
3✔
438
        if err != nil {
3✔
439
                return nil, err
×
440
        }
×
441

442
        // We'll track our local close output, even if it's dust in BTC terms,
443
        // it might still carry value in custom channel terms.
444
        _, dustAmt := c.cfg.Channel.LocalBalanceDust()
3✔
445
        localBalance, _ := c.cfg.Channel.CommitBalances()
3✔
446
        c.localCloseOutput = fn.Some(CloseOutput{
3✔
447
                Amt:             localBalance,
3✔
448
                DustLimit:       dustAmt,
3✔
449
                PkScript:        c.localDeliveryScript,
3✔
450
                ShutdownRecords: shutdown.CustomRecords,
3✔
451
        })
3✔
452

3✔
453
        return shutdown, nil
3✔
454
}
455

456
// ShutdownChan is the first method that's to be called by the initiator of the
457
// cooperative channel closure. This message returns the shutdown message to
458
// send to the remote party. Upon completion, we enter the
459
// closeShutdownInitiated phase as we await a response.
460
func (c *ChanCloser) ShutdownChan() (*lnwire.Shutdown, error) {
3✔
461
        // If we attempt to shutdown the channel for the first time, and we're not
3✔
462
        // in the closeIdle state, then the caller made an error.
3✔
463
        if c.state != closeIdle {
3✔
464
                return nil, ErrChanAlreadyClosing
×
465
        }
×
466

467
        chancloserLog.Infof("ChannelPoint(%v): initiating shutdown", c.chanPoint)
3✔
468

3✔
469
        shutdownMsg, err := c.initChanShutdown()
3✔
470
        if err != nil {
3✔
471
                return nil, err
×
472
        }
×
473

474
        // With the opening steps complete, we'll transition into the
475
        // closeShutdownInitiated state. In this state, we'll wait until the
476
        // other party sends their version of the shutdown message.
477
        c.state = closeShutdownInitiated
3✔
478

3✔
479
        // Finally, we'll return the shutdown message to the caller so it can
3✔
480
        // send it to the remote peer.
3✔
481
        return shutdownMsg, nil
3✔
482
}
483

484
// ClosingTx returns the fully signed, final closing transaction.
485
//
486
// NOTE: This transaction is only available if the state machine is in the
487
// closeFinished state.
488
func (c *ChanCloser) ClosingTx() (*wire.MsgTx, error) {
3✔
489
        // If the state machine hasn't finished closing the channel, then we'll
3✔
490
        // return an error as we haven't yet computed the closing tx.
3✔
491
        if c.state != closeFinished {
6✔
492
                return nil, ErrChanCloseNotFinished
3✔
493
        }
3✔
494

495
        return c.closingTx, nil
3✔
496
}
497

498
// CloseRequest returns the original close request that prompted the creation
499
// of the state machine.
500
//
501
// NOTE: This will only return a non-nil pointer if we were the initiator of
502
// the cooperative closure workflow.
503
func (c *ChanCloser) CloseRequest() *htlcswitch.ChanClose {
3✔
504
        return c.closeReq
3✔
505
}
3✔
506

507
// Channel returns the channel stored in the config as a
508
// *lnwallet.LightningChannel.
509
//
510
// NOTE: This method will PANIC if the underlying channel implementation isn't
511
// the desired type.
512
func (c *ChanCloser) Channel() *lnwallet.LightningChannel {
3✔
513
        // TODO(roasbeef): remove this
3✔
514
        return c.cfg.Channel.(*lnwallet.LightningChannel)
3✔
515
}
3✔
516

517
// NegotiationHeight returns the negotiation height.
518
func (c *ChanCloser) NegotiationHeight() uint32 {
3✔
519
        return c.negotiationHeight
3✔
520
}
3✔
521

522
// LocalCloseOutput returns the local close output.
523
func (c *ChanCloser) LocalCloseOutput() fn.Option[CloseOutput] {
3✔
524
        return c.localCloseOutput
3✔
525
}
3✔
526

527
// RemoteCloseOutput returns the remote close output.
528
func (c *ChanCloser) RemoteCloseOutput() fn.Option[CloseOutput] {
3✔
529
        return c.remoteCloseOutput
3✔
530
}
3✔
531

532
// AuxOutputs returns optional extra outputs.
533
func (c *ChanCloser) AuxOutputs() fn.Option[AuxCloseOutputs] {
3✔
534
        return c.auxOutputs
3✔
535
}
3✔
536

537
// validateShutdownScript attempts to match and validate the script provided in
538
// our peer's shutdown message with the upfront shutdown script we have on
539
// record. For any script specified, we also make sure it matches our
540
// requirements. If no upfront shutdown script was set, we do not need to
541
// enforce option upfront shutdown, so the function returns early. If an
542
// upfront script is set, we check whether it matches the script provided by
543
// our peer. If they do not match, we use the disconnect function provided to
544
// disconnect from the peer.
545
func validateShutdownScript(upfrontScript, peerScript lnwire.DeliveryAddress,
546
        netParams *chaincfg.Params) error {
3✔
547

3✔
548
        // Either way, we'll make sure that the script passed meets our
3✔
549
        // standards. The upfrontScript should have already been checked at an
3✔
550
        // earlier stage, but we'll repeat the check here for defense in depth.
3✔
551
        if len(upfrontScript) != 0 {
6✔
552
                if !lnwallet.ValidateUpfrontShutdown(upfrontScript, netParams) {
3✔
553
                        return ErrInvalidShutdownScript
×
554
                }
×
555
        }
556
        if len(peerScript) != 0 {
6✔
557
                if !lnwallet.ValidateUpfrontShutdown(peerScript, netParams) {
3✔
UNCOV
558
                        return ErrInvalidShutdownScript
×
UNCOV
559
                }
×
560
        }
561

562
        // If no upfront shutdown script was set, return early because we do
563
        // not need to enforce closure to a specific script.
564
        if len(upfrontScript) == 0 {
6✔
565
                return nil
3✔
566
        }
3✔
567

568
        // If an upfront shutdown script was provided, disconnect from the peer, as
569
        // per BOLT 2, and return an error.
570
        if !bytes.Equal(upfrontScript, peerScript) {
3✔
UNCOV
571
                chancloserLog.Warnf("peer's script: %x does not match upfront "+
×
UNCOV
572
                        "shutdown script: %x", peerScript, upfrontScript)
×
UNCOV
573

×
UNCOV
574
                return ErrUpfrontShutdownScriptMismatch
×
UNCOV
575
        }
×
576

577
        return nil
3✔
578
}
579

580
// ReceiveShutdown takes a raw Shutdown message and uses it to try and advance
581
// the ChanCloser state machine, failing if it is coming in at an invalid time.
582
// If appropriate, it will also generate a Shutdown message of its own to send
583
// out to the peer. It is possible for this method to return None when no error
584
// occurred.
585
func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) (
586
        fn.Option[lnwire.Shutdown], error) {
3✔
587

3✔
588
        noShutdown := fn.None[lnwire.Shutdown]()
3✔
589

3✔
590
        // We'll track their remote close output, even if it's dust in BTC
3✔
591
        // terms, it might still carry value in custom channel terms.
3✔
592
        _, dustAmt := c.cfg.Channel.RemoteBalanceDust()
3✔
593
        _, remoteBalance := c.cfg.Channel.CommitBalances()
3✔
594
        c.remoteCloseOutput = fn.Some(CloseOutput{
3✔
595
                Amt:             remoteBalance,
3✔
596
                DustLimit:       dustAmt,
3✔
597
                PkScript:        msg.Address,
3✔
598
                ShutdownRecords: msg.CustomRecords,
3✔
599
        })
3✔
600

3✔
601
        switch c.state {
3✔
602
        // If we're in the close idle state, and we're receiving a channel
603
        // closure related message, then this indicates that we're on the
604
        // receiving side of an initiated channel closure.
605
        case closeIdle:
3✔
606
                // As we're the responder to this shutdown (the other party
3✔
607
                // wants to close), we'll check if this is a frozen channel or
3✔
608
                // not. If the channel is frozen and we were not also the
3✔
609
                // initiator of the channel opening, then we'll deny their close
3✔
610
                // attempt.
3✔
611
                chanInitiator := c.cfg.Channel.IsInitiator()
3✔
612
                if !chanInitiator {
6✔
613
                        absoluteThawHeight, err :=
3✔
614
                                c.cfg.Channel.AbsoluteThawHeight()
3✔
615
                        if err != nil {
3✔
616
                                return noShutdown, err
×
617
                        }
×
618
                        if c.negotiationHeight < absoluteThawHeight {
3✔
619
                                return noShutdown, fmt.Errorf("initiator "+
×
620
                                        "attempting to co-op close frozen "+
×
621
                                        "ChannelPoint(%v) (current_height=%v, "+
×
622
                                        "thaw_height=%v)", c.chanPoint,
×
623
                                        c.negotiationHeight, absoluteThawHeight)
×
624
                        }
×
625
                }
626

627
                // If the remote node opened the channel with option upfront
628
                // shutdown script, check that the script they provided matches.
629
                if err := validateShutdownScript(
3✔
630
                        c.cfg.Channel.RemoteUpfrontShutdownScript(),
3✔
631
                        msg.Address, c.cfg.ChainParams,
3✔
632
                ); err != nil {
3✔
633
                        return noShutdown, err
×
634
                }
×
635

636
                // Once we have checked that the other party has not violated
637
                // option upfront shutdown we set their preference for delivery
638
                // address. We'll use this when we craft the closure
639
                // transaction.
640
                c.remoteDeliveryScript = msg.Address
3✔
641

3✔
642
                // We'll generate a shutdown message of our own to send across
3✔
643
                // the wire.
3✔
644
                localShutdown, err := c.initChanShutdown()
3✔
645
                if err != nil {
3✔
646
                        return noShutdown, err
×
647
                }
×
648

649
                // If this is a taproot channel, then we'll want to stash the
650
                // remote nonces so we can properly create a new musig
651
                // session for signing.
652
                if c.cfg.Channel.ChanType().IsTaproot() {
6✔
653
                        shutdownNonce, err := msg.ShutdownNonce.UnwrapOrErrV(
3✔
654
                                errNoShutdownNonce,
3✔
655
                        )
3✔
656
                        if err != nil {
3✔
657
                                return noShutdown, err
×
658
                        }
×
659

660
                        c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{
3✔
661
                                PubNonce: shutdownNonce,
3✔
662
                        })
3✔
663
                }
664

665
                chancloserLog.Infof("ChannelPoint(%v): responding to shutdown",
3✔
666
                        c.chanPoint)
3✔
667

3✔
668
                // After the other party receives this message, we'll actually
3✔
669
                // start the final stage of the closure process: fee
3✔
670
                // negotiation. So we'll update our internal state to reflect
3✔
671
                // this, so we can handle the next message sent.
3✔
672
                c.state = closeAwaitingFlush
3✔
673

3✔
674
                return fn.Some(*localShutdown), err
3✔
675

676
        case closeShutdownInitiated:
3✔
677
                // If the remote node opened the channel with option upfront
3✔
678
                // shutdown script, check that the script they provided matches.
3✔
679
                if err := validateShutdownScript(
3✔
680
                        c.cfg.Channel.RemoteUpfrontShutdownScript(),
3✔
681
                        msg.Address, c.cfg.ChainParams,
3✔
682
                ); err != nil {
3✔
683
                        return noShutdown, err
×
684
                }
×
685

686
                // Now that we know this is a valid shutdown message and
687
                // address, we'll record their preferred delivery closing
688
                // script.
689
                c.remoteDeliveryScript = msg.Address
3✔
690

3✔
691
                // At this point, we can now start the fee negotiation state, by
3✔
692
                // constructing and sending our initial signature for what we
3✔
693
                // think the closing transaction should look like.
3✔
694
                c.state = closeAwaitingFlush
3✔
695

3✔
696
                // If this is a taproot channel, then we'll want to stash the
3✔
697
                // local+remote nonces so we can properly create a new musig
3✔
698
                // session for signing.
3✔
699
                if c.cfg.Channel.ChanType().IsTaproot() {
6✔
700
                        shutdownNonce, err := msg.ShutdownNonce.UnwrapOrErrV(
3✔
701
                                errNoShutdownNonce,
3✔
702
                        )
3✔
703
                        if err != nil {
3✔
704
                                return noShutdown, err
×
705
                        }
×
706

707
                        c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{
3✔
708
                                PubNonce: shutdownNonce,
3✔
709
                        })
3✔
710
                }
711

712
                chancloserLog.Infof("ChannelPoint(%v): shutdown response "+
3✔
713
                        "received, entering fee negotiation", c.chanPoint)
3✔
714

3✔
715
                return noShutdown, nil
3✔
716

717
        default:
×
718
                // Otherwise we are not in a state where we can accept this
×
719
                // message.
×
720
                return noShutdown, ErrInvalidState
×
721
        }
722
}
723

724
// BeginNegotiation should be called when we have definitively reached a clean
725
// channel state and are ready to cooperatively arrive at a closing transaction.
726
// If it is our responsibility to kick off the negotiation, this method will
727
// generate a ClosingSigned message. If it is the remote's responsibility, then
728
// it will not. In either case it will transition the ChanCloser state machine
729
// to the negotiation phase wherein ClosingSigned messages are exchanged until
730
// a mutually agreeable result is achieved.
731
func (c *ChanCloser) BeginNegotiation() (fn.Option[lnwire.ClosingSigned],
732
        error) {
3✔
733

3✔
734
        noClosingSigned := fn.None[lnwire.ClosingSigned]()
3✔
735

3✔
736
        switch c.state {
3✔
737
        case closeAwaitingFlush:
3✔
738
                // Now that we know their desired delivery script, we can
3✔
739
                // compute what our max/ideal fee will be.
3✔
740
                c.initFeeBaseline()
3✔
741

3✔
742
                // Before continuing, mark the channel as cooperatively closed
3✔
743
                // with a nil txn. Even though we haven't negotiated the final
3✔
744
                // txn, this guarantees that our listchannels rpc will be
3✔
745
                // externally consistent, and reflect that the channel is being
3✔
746
                // shutdown by the time the closing request returns.
3✔
747
                err := c.cfg.Channel.MarkCoopBroadcasted(
3✔
748
                        nil, c.closer,
3✔
749
                )
3✔
750
                if err != nil {
3✔
751
                        return noClosingSigned, err
×
752
                }
×
753

754
                // At this point, we can now start the fee negotiation state, by
755
                // constructing and sending our initial signature for what we
756
                // think the closing transaction should look like.
757
                c.state = closeFeeNegotiation
3✔
758

3✔
759
                if !c.cfg.Channel.IsInitiator() {
6✔
760
                        // By default this means we do nothing, but we do want
3✔
761
                        // to check if we have a cached remote offer to process.
3✔
762
                        // If we do, we'll process it here.
3✔
763
                        res := noClosingSigned
3✔
764
                        err = nil
3✔
765
                        c.cachedClosingSigned.WhenSome(
3✔
766
                                func(cs lnwire.ClosingSigned) {
6✔
767
                                        res, err = c.ReceiveClosingSigned(cs)
3✔
768
                                },
3✔
769
                        )
770

771
                        return res, err
3✔
772
                }
773

774
                // We'll craft our initial close proposal in order to keep the
775
                // negotiation moving, but only if we're the initiator.
776
                closingSigned, err := c.proposeCloseSigned(c.idealFeeSat)
3✔
777
                if err != nil {
3✔
778
                        return noClosingSigned,
×
779
                                fmt.Errorf("unable to sign new co op "+
×
780
                                        "close offer: %w", err)
×
781
                }
×
782

783
                return fn.Some(*closingSigned), nil
3✔
784

785
        default:
×
786
                return noClosingSigned, ErrInvalidState
×
787
        }
788
}
789

790
// ReceiveClosingSigned is a method that should be called whenever we receive a
791
// ClosingSigned message from the wire. It may or may not return a
792
// ClosingSigned of our own to send back to the remote.
793
func (c *ChanCloser) ReceiveClosingSigned( //nolint:funlen
794
        msg lnwire.ClosingSigned) (fn.Option[lnwire.ClosingSigned], error) {
3✔
795

3✔
796
        noClosing := fn.None[lnwire.ClosingSigned]()
3✔
797

3✔
798
        switch c.state {
3✔
799
        case closeAwaitingFlush:
3✔
800
                // If we hit this case it either means there's a protocol
3✔
801
                // violation or that our chanCloser received the remote offer
3✔
802
                // before the link finished processing the channel flush.
3✔
803
                c.cachedClosingSigned = fn.Some(msg)
3✔
804
                return fn.None[lnwire.ClosingSigned](), nil
3✔
805

806
        case closeFeeNegotiation:
3✔
807
                // If this is a taproot channel, then it MUST have a partial
3✔
808
                // signature set at this point.
3✔
809
                isTaproot := c.cfg.Channel.ChanType().IsTaproot()
3✔
810
                if isTaproot && msg.PartialSig.IsNone() {
3✔
811
                        return noClosing,
×
812
                                fmt.Errorf("partial sig not set " +
×
813
                                        "for taproot chan")
×
814
                }
×
815

816
                isInitiator := c.cfg.Channel.IsInitiator()
3✔
817

3✔
818
                // We'll compare the proposed total fee, to what we've proposed
3✔
819
                // during the negotiations. If it doesn't match any of our
3✔
820
                // prior offers, then we'll attempt to ratchet the fee closer
3✔
821
                // to our ideal fee.
3✔
822
                remoteProposedFee := msg.FeeSatoshis
3✔
823

3✔
824
                _, feeMatchesOffer := c.priorFeeOffers[remoteProposedFee]
3✔
825
                switch {
3✔
826
                // For taproot channels, since nonces are involved, we can't do
827
                // the existing co-op close negotiation process without going
828
                // to a fully round based model. Rather than do this, we'll
829
                // just accept the very first offer by the initiator.
830
                case isTaproot && !isInitiator:
3✔
831
                        chancloserLog.Infof("ChannelPoint(%v) accepting "+
3✔
832
                                "initiator fee of %v", c.chanPoint,
3✔
833
                                remoteProposedFee)
3✔
834

3✔
835
                        // To auto-accept the initiators proposal, we'll just
3✔
836
                        // send back a signature w/ the same offer. We don't
3✔
837
                        // send the message here, as we can drop down and
3✔
838
                        // finalize the closure and broadcast, then echo back
3✔
839
                        // to Alice the final signature.
3✔
840
                        _, err := c.proposeCloseSigned(remoteProposedFee)
3✔
841
                        if err != nil {
3✔
842
                                return noClosing, fmt.Errorf("unable to sign "+
×
843
                                        "new co op close offer: %w", err)
×
844
                        }
×
845

846
                // Otherwise, if we are the initiator, and we just sent a
847
                // signature for a taproot channel, then we'll ensure that the
848
                // fee rate matches up exactly.
UNCOV
849
                case isTaproot && isInitiator && !feeMatchesOffer:
×
UNCOV
850
                        return noClosing,
×
UNCOV
851
                                fmt.Errorf("fee rate for "+
×
UNCOV
852
                                        "taproot channels was not accepted: "+
×
UNCOV
853
                                        "sent %v, got %v",
×
UNCOV
854
                                        c.idealFeeSat, remoteProposedFee)
×
855

856
                // If we're the initiator of the taproot channel, and we had
857
                // our fee echo'd back, then it's all good, and we can proceed
858
                // with final broadcast.
859
                case isTaproot && isInitiator && feeMatchesOffer:
3✔
860
                        break
3✔
861

862
                // Otherwise, if this is a normal segwit v0 channel, and the
863
                // fee doesn't match our offer, then we'll try to "negotiate" a
864
                // new fee.
865
                case !feeMatchesOffer:
3✔
866
                        // We'll now attempt to ratchet towards a fee deemed
3✔
867
                        // acceptable by both parties, factoring in our ideal
3✔
868
                        // fee rate, and the last proposed fee by both sides.
3✔
869
                        proposal := calcCompromiseFee(
3✔
870
                                c.chanPoint, c.idealFeeSat, c.lastFeeProposal,
3✔
871
                                remoteProposedFee,
3✔
872
                        )
3✔
873
                        if c.cfg.Channel.IsInitiator() && proposal > c.maxFee {
3✔
UNCOV
874
                                return noClosing, fmt.Errorf(
×
UNCOV
875
                                        "%w: %v > %v",
×
UNCOV
876
                                        ErrProposalExceedsMaxFee,
×
UNCOV
877
                                        proposal, c.maxFee)
×
UNCOV
878
                        }
×
879

880
                        // With our new fee proposal calculated, we'll craft a
881
                        // new close signed signature to send to the other
882
                        // party so we can continue the fee negotiation
883
                        // process.
884
                        closeSigned, err := c.proposeCloseSigned(proposal)
3✔
885
                        if err != nil {
3✔
UNCOV
886
                                return noClosing, fmt.Errorf("unable to sign "+
×
UNCOV
887
                                        "new co op close offer: %w", err)
×
UNCOV
888
                        }
×
889

890
                        // If the compromise fee doesn't match what the peer
891
                        // proposed, then we'll return this latest close signed
892
                        // message so we can continue negotiation.
893
                        if proposal != remoteProposedFee {
3✔
UNCOV
894
                                chancloserLog.Debugf("ChannelPoint(%v): close "+
×
UNCOV
895
                                        "tx fee disagreement, continuing "+
×
UNCOV
896
                                        "negotiation", c.chanPoint)
×
UNCOV
897

×
UNCOV
898
                                return fn.Some(*closeSigned), nil
×
UNCOV
899
                        }
×
900
                }
901

902
                chancloserLog.Infof("ChannelPoint(%v) fee of %v accepted, "+
3✔
903
                        "ending negotiation", c.chanPoint, remoteProposedFee)
3✔
904

3✔
905
                // Otherwise, we've agreed on a fee for the closing
3✔
906
                // transaction! We'll craft the final closing transaction so we
3✔
907
                // can broadcast it to the network.
3✔
908
                var (
3✔
909
                        localSig, remoteSig input.Signature
3✔
910
                        closeOpts           []lnwallet.ChanCloseOpt
3✔
911
                        err                 error
3✔
912
                )
3✔
913
                matchingSig := c.priorFeeOffers[remoteProposedFee]
3✔
914
                if c.cfg.Channel.ChanType().IsTaproot() {
6✔
915
                        localWireSig, err := matchingSig.PartialSig.UnwrapOrErrV( //nolint:ll
3✔
916
                                fmt.Errorf("none local sig"),
3✔
917
                        )
3✔
918
                        if err != nil {
3✔
919
                                return noClosing, err
×
920
                        }
×
921
                        remoteWireSig, err := msg.PartialSig.UnwrapOrErrV(
3✔
922
                                fmt.Errorf("none remote sig"),
3✔
923
                        )
3✔
924
                        if err != nil {
3✔
925
                                return noClosing, err
×
926
                        }
×
927

928
                        muSession := c.cfg.MusigSession
3✔
929
                        localSig, remoteSig, closeOpts, err = muSession.CombineClosingOpts( //nolint:ll
3✔
930
                                localWireSig, remoteWireSig,
3✔
931
                        )
3✔
932
                        if err != nil {
3✔
933
                                return noClosing, err
×
934
                        }
×
935
                } else {
3✔
936
                        localSig, err = matchingSig.Signature.ToSignature()
3✔
937
                        if err != nil {
3✔
938
                                return noClosing, err
×
939
                        }
×
940
                        remoteSig, err = msg.Signature.ToSignature()
3✔
941
                        if err != nil {
3✔
942
                                return noClosing, err
×
943
                        }
×
944
                }
945

946
                // Before we complete the cooperative close, we'll see if we
947
                // have any extra aux options.
948
                c.auxOutputs, err = c.auxCloseOutputs(remoteProposedFee)
3✔
949
                if err != nil {
3✔
950
                        return noClosing, err
×
951
                }
×
952
                c.auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
3✔
953
                        closeOpts = append(
×
954
                                closeOpts, lnwallet.WithExtraCloseOutputs(
×
955
                                        outs.ExtraCloseOutputs,
×
956
                                ),
×
957
                        )
×
958
                        closeOpts = append(
×
959
                                closeOpts, lnwallet.WithCustomCoopSort(
×
960
                                        outs.CustomSort,
×
961
                                ),
×
962
                        )
×
963
                })
×
964

965
                closeTx, _, err := c.cfg.Channel.CompleteCooperativeClose(
3✔
966
                        localSig, remoteSig, c.localDeliveryScript,
3✔
967
                        c.remoteDeliveryScript, remoteProposedFee, closeOpts...,
3✔
968
                )
3✔
969
                if err != nil {
3✔
970
                        return noClosing, err
×
971
                }
×
972
                c.closingTx = closeTx
3✔
973

3✔
974
                // If there's an aux chan closer, then we'll finalize with it
3✔
975
                // before we write to disk.
3✔
976
                err = fn.MapOptionZ(
3✔
977
                        c.cfg.AuxCloser, func(aux AuxChanCloser) error {
3✔
978
                                channel := c.cfg.Channel
×
979
                                //nolint:ll
×
980
                                req := AuxShutdownReq{
×
981
                                        ChanPoint:   c.chanPoint,
×
982
                                        ShortChanID: c.cfg.Channel.ShortChanID(),
×
983
                                        InternalKey: c.localInternalKey,
×
984
                                        Initiator:   channel.IsInitiator(),
×
985
                                        CommitBlob:  channel.LocalCommitmentBlob(),
×
986
                                        FundingBlob: channel.FundingBlob(),
×
987
                                }
×
988
                                desc := AuxCloseDesc{
×
989
                                        AuxShutdownReq:    req,
×
990
                                        LocalCloseOutput:  c.localCloseOutput,
×
991
                                        RemoteCloseOutput: c.remoteCloseOutput,
×
992
                                }
×
993

×
994
                                return aux.FinalizeClose(desc, closeTx)
×
995
                        },
×
996
                )
997
                if err != nil {
3✔
998
                        return noClosing, err
×
999
                }
×
1000

1001
                // Before publishing the closing tx, we persist it to the
1002
                // database, such that it can be republished if something goes
1003
                // wrong.
1004
                err = c.cfg.Channel.MarkCoopBroadcasted(
3✔
1005
                        closeTx, c.closer,
3✔
1006
                )
3✔
1007
                if err != nil {
4✔
1008
                        return noClosing, err
1✔
1009
                }
1✔
1010

1011
                // With the closing transaction crafted, we'll now broadcast it
1012
                // to the network.
1013
                chancloserLog.Infof("Broadcasting cooperative close tx: %v",
3✔
1014
                        lnutils.SpewLogClosure(closeTx))
3✔
1015

3✔
1016
                // Create a close channel label.
3✔
1017
                chanID := c.cfg.Channel.ShortChanID()
3✔
1018
                closeLabel := labels.MakeLabel(
3✔
1019
                        labels.LabelTypeChannelClose, &chanID,
3✔
1020
                )
3✔
1021

3✔
1022
                if err := c.cfg.BroadcastTx(closeTx, closeLabel); err != nil {
3✔
1023
                        return noClosing, err
×
1024
                }
×
1025

1026
                // Finally, we'll transition to the closeFinished state, and
1027
                // also return the final close signed message we sent.
1028
                // Additionally, we return true for the second argument to
1029
                // indicate we're finished with the channel closing
1030
                // negotiation.
1031
                c.state = closeFinished
3✔
1032
                matchingOffer := c.priorFeeOffers[remoteProposedFee]
3✔
1033

3✔
1034
                return fn.Some(*matchingOffer), nil
3✔
1035

1036
        // If we received a message while in the closeFinished state, then this
1037
        // should only be the remote party echoing the last ClosingSigned
1038
        // message that we agreed on.
1039
        case closeFinished:
3✔
1040

3✔
1041
                // There's no more to do as both sides should have already
3✔
1042
                // broadcast the closing transaction at this state.
3✔
1043
                return noClosing, nil
3✔
1044

1045
        default:
×
1046
                return noClosing, ErrInvalidState
×
1047
        }
1048
}
1049

1050
// auxCloseOutputs returns any additional outputs that should be used when
1051
// closing the channel.
1052
func (c *ChanCloser) auxCloseOutputs(
1053
        closeFee btcutil.Amount) (fn.Option[AuxCloseOutputs], error) {
3✔
1054

3✔
1055
        var closeOuts fn.Option[AuxCloseOutputs]
3✔
1056
        err := fn.MapOptionZ(c.cfg.AuxCloser, func(aux AuxChanCloser) error {
3✔
1057
                req := AuxShutdownReq{
×
1058
                        ChanPoint:   c.chanPoint,
×
1059
                        ShortChanID: c.cfg.Channel.ShortChanID(),
×
1060
                        InternalKey: c.localInternalKey,
×
1061
                        Initiator:   c.cfg.Channel.IsInitiator(),
×
1062
                        CommitBlob:  c.cfg.Channel.LocalCommitmentBlob(),
×
1063
                        FundingBlob: c.cfg.Channel.FundingBlob(),
×
1064
                }
×
1065
                outs, err := aux.AuxCloseOutputs(AuxCloseDesc{
×
1066
                        AuxShutdownReq:    req,
×
1067
                        CloseFee:          closeFee,
×
1068
                        CommitFee:         c.cfg.Channel.CommitFee(),
×
1069
                        LocalCloseOutput:  c.localCloseOutput,
×
1070
                        RemoteCloseOutput: c.remoteCloseOutput,
×
1071
                })
×
1072
                if err != nil {
×
1073
                        return err
×
1074
                }
×
1075

1076
                closeOuts = outs
×
1077

×
1078
                return nil
×
1079
        })
1080
        if err != nil {
3✔
1081
                return closeOuts, err
×
1082
        }
×
1083

1084
        return closeOuts, nil
3✔
1085
}
1086

1087
// proposeCloseSigned attempts to propose a new signature for the closing
1088
// transaction for a channel based on the prior fee negotiations and our
1089
// current compromise fee.
1090
func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (
1091
        *lnwire.ClosingSigned, error) {
3✔
1092

3✔
1093
        var (
3✔
1094
                closeOpts []lnwallet.ChanCloseOpt
3✔
1095
                err       error
3✔
1096
        )
3✔
1097

3✔
1098
        // If this is a taproot channel, then we'll include the musig session
3✔
1099
        // generated for the next co-op close negotiation round.
3✔
1100
        if c.cfg.Channel.ChanType().IsTaproot() {
6✔
1101
                closeOpts, err = c.cfg.MusigSession.ProposalClosingOpts()
3✔
1102
                if err != nil {
3✔
1103
                        return nil, err
×
1104
                }
×
1105
        }
1106

1107
        // We'll also now see if the aux chan closer has any additional options
1108
        // for the closing purpose.
1109
        c.auxOutputs, err = c.auxCloseOutputs(fee)
3✔
1110
        if err != nil {
3✔
1111
                return nil, err
×
1112
        }
×
1113
        c.auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
3✔
1114
                closeOpts = append(
×
1115
                        closeOpts, lnwallet.WithExtraCloseOutputs(
×
1116
                                outs.ExtraCloseOutputs,
×
1117
                        ),
×
1118
                )
×
1119
                closeOpts = append(
×
1120
                        closeOpts, lnwallet.WithCustomCoopSort(
×
1121
                                outs.CustomSort,
×
1122
                        ),
×
1123
                )
×
1124
        })
×
1125

1126
        // With all our options added, we'll attempt to co-op close now.
1127
        rawSig, _, _, err := c.cfg.Channel.CreateCloseProposal(
3✔
1128
                fee, c.localDeliveryScript, c.remoteDeliveryScript,
3✔
1129
                closeOpts...,
3✔
1130
        )
3✔
1131
        if err != nil {
3✔
1132
                return nil, err
×
1133
        }
×
1134

1135
        // We'll note our last signature and proposed fee so when the remote
1136
        // party responds we'll be able to decide if we've agreed on fees or
1137
        // not.
1138
        var (
3✔
1139
                parsedSig  lnwire.Sig
3✔
1140
                partialSig *lnwire.PartialSigWithNonce
3✔
1141
        )
3✔
1142
        if c.cfg.Channel.ChanType().IsTaproot() {
6✔
1143
                musig, ok := rawSig.(*lnwallet.MusigPartialSig)
3✔
1144
                if !ok {
3✔
1145
                        return nil, fmt.Errorf("expected MusigPartialSig, "+
×
1146
                                "got %T", rawSig)
×
1147
                }
×
1148

1149
                partialSig = musig.ToWireSig()
3✔
1150
        } else {
3✔
1151
                parsedSig, err = lnwire.NewSigFromSignature(rawSig)
3✔
1152
                if err != nil {
3✔
UNCOV
1153
                        return nil, err
×
UNCOV
1154
                }
×
1155
        }
1156

1157
        c.lastFeeProposal = fee
3✔
1158

3✔
1159
        chancloserLog.Infof("ChannelPoint(%v): proposing fee of %v sat to "+
3✔
1160
                "close chan", c.chanPoint, int64(fee))
3✔
1161

3✔
1162
        // We'll assemble a ClosingSigned message using this information and
3✔
1163
        // return it to the caller so we can kick off the final stage of the
3✔
1164
        // channel closure process.
3✔
1165
        closeSignedMsg := lnwire.NewClosingSigned(c.cid, fee, parsedSig)
3✔
1166

3✔
1167
        // For musig2 channels, the main sig is blank, and instead we'll send
3✔
1168
        // over a partial signature which'll be combined once our offer is
3✔
1169
        // accepted.
3✔
1170
        if partialSig != nil {
6✔
1171
                closeSignedMsg.PartialSig = lnwire.SomePartialSig(
3✔
1172
                        partialSig.PartialSig,
3✔
1173
                )
3✔
1174
        }
3✔
1175

1176
        // We'll also save this close signed, in the case that the remote party
1177
        // accepts our offer. This way, we don't have to re-sign.
1178
        c.priorFeeOffers[fee] = closeSignedMsg
3✔
1179

3✔
1180
        return closeSignedMsg, nil
3✔
1181
}
1182

1183
// feeInAcceptableRange returns true if the passed remote fee is deemed to be
1184
// in an "acceptable" range to our local fee. This is an attempt at a
1185
// compromise and to ensure that the fee negotiation has a stopping point. We
1186
// consider their fee acceptable if it's within 30% of our fee.
UNCOV
1187
func feeInAcceptableRange(localFee, remoteFee btcutil.Amount) bool {
×
UNCOV
1188
        // If our offer is lower than theirs, then we'll accept their offer if
×
UNCOV
1189
        // it's no more than 30% *greater* than our current offer.
×
UNCOV
1190
        if localFee < remoteFee {
×
UNCOV
1191
                acceptableRange := localFee + ((localFee * 3) / 10)
×
UNCOV
1192
                return remoteFee <= acceptableRange
×
UNCOV
1193
        }
×
1194

1195
        // If our offer is greater than theirs, then we'll accept their offer if
1196
        // it's no more than 30% *less* than our current offer.
1197
        acceptableRange := localFee - ((localFee * 3) / 10)
×
1198
        return remoteFee >= acceptableRange
×
1199
}
1200

1201
// ratchetFee is our step function used to inch our fee closer to something
1202
// that both sides can agree on. If up is true, then we'll attempt to increase
1203
// our offered fee. Otherwise, if up is false, then we'll attempt to decrease
1204
// our offered fee.
UNCOV
1205
func ratchetFee(fee btcutil.Amount, up bool) btcutil.Amount {
×
UNCOV
1206
        // If we need to ratchet up, then we'll increase our fee by 10%.
×
UNCOV
1207
        if up {
×
UNCOV
1208
                return fee + ((fee * 1) / 10)
×
UNCOV
1209
        }
×
1210

1211
        // Otherwise, we'll *decrease* our fee by 10%.
1212
        return fee - ((fee * 1) / 10)
×
1213
}
1214

1215
// calcCompromiseFee performs the current fee negotiation algorithm, taking
1216
// into consideration our ideal fee based on current fee environment, the fee
1217
// we last proposed (if any), and the fee proposed by the peer.
1218
func calcCompromiseFee(chanPoint wire.OutPoint, ourIdealFee, lastSentFee,
1219
        remoteFee btcutil.Amount) btcutil.Amount {
3✔
1220

3✔
1221
        // TODO(roasbeef): take in number of rounds as well?
3✔
1222

3✔
1223
        chancloserLog.Infof("ChannelPoint(%v): computing fee compromise, "+
3✔
1224
                "ideal=%v, last_sent=%v, remote_offer=%v", chanPoint,
3✔
1225
                int64(ourIdealFee), int64(lastSentFee), int64(remoteFee))
3✔
1226

3✔
1227
        // Otherwise, we'll need to attempt to make a fee compromise if this is
3✔
1228
        // the second round, and neither side has agreed on fees.
3✔
1229
        switch {
3✔
1230
        // If their proposed fee is identical to our ideal fee, then we'll go
1231
        // with that as we can short circuit the fee negotiation. Similarly, if
1232
        // we haven't sent an offer yet, we'll default to our ideal fee.
1233
        case ourIdealFee == remoteFee || lastSentFee == 0:
3✔
1234
                return ourIdealFee
3✔
1235

1236
        // If the last fee we sent, is equal to the fee the remote party is
1237
        // offering, then we can simply return this fee as the negotiation is
1238
        // over.
1239
        case remoteFee == lastSentFee:
×
1240
                return lastSentFee
×
1241

1242
        // If the fee the remote party is offering is less than the last one we
1243
        // sent, then we'll need to ratchet down in order to move our offer
1244
        // closer to theirs.
1245
        case remoteFee < lastSentFee:
×
1246
                // If the fee is lower, but still acceptable, then we'll just
×
1247
                // return this fee and end the negotiation.
×
1248
                if feeInAcceptableRange(lastSentFee, remoteFee) {
×
1249
                        chancloserLog.Infof("ChannelPoint(%v): proposed "+
×
1250
                                "remote fee is close enough, capitulating",
×
1251
                                chanPoint)
×
1252

×
1253
                        return remoteFee
×
1254
                }
×
1255

1256
                // Otherwise, we'll ratchet the fee *down* using our current
1257
                // algorithm.
1258
                return ratchetFee(lastSentFee, false)
×
1259

1260
        // If the fee the remote party is offering is greater than the last one
1261
        // we sent, then we'll ratchet up in order to ensure we terminate
1262
        // eventually.
UNCOV
1263
        case remoteFee > lastSentFee:
×
UNCOV
1264
                // If the fee is greater, but still acceptable, then we'll just
×
UNCOV
1265
                // return this fee in order to put an end to the negotiation.
×
UNCOV
1266
                if feeInAcceptableRange(lastSentFee, remoteFee) {
×
1267
                        chancloserLog.Infof("ChannelPoint(%v): proposed "+
×
1268
                                "remote fee is close enough, capitulating",
×
1269
                                chanPoint)
×
1270

×
1271
                        return remoteFee
×
1272
                }
×
1273

1274
                // Otherwise, we'll ratchet the fee up using our current
1275
                // algorithm.
UNCOV
1276
                return ratchetFee(lastSentFee, true)
×
1277

1278
        default:
×
1279
                // TODO(roasbeef): fail if their fee isn't in expected range
×
1280
                return remoteFee
×
1281
        }
1282
}
1283

1284
// ParseUpfrontShutdownAddress attempts to parse an upfront shutdown address.
1285
// If the address is empty, it returns nil. If it successfully decoded the
1286
// address, it returns a script that pays out to the address.
1287
func ParseUpfrontShutdownAddress(address string,
1288
        params *chaincfg.Params) (lnwire.DeliveryAddress, error) {
3✔
1289

3✔
1290
        if len(address) == 0 {
6✔
1291
                return nil, nil
3✔
1292
        }
3✔
1293

1294
        addr, err := btcutil.DecodeAddress(
3✔
1295
                address, params,
3✔
1296
        )
3✔
1297
        if err != nil {
3✔
UNCOV
1298
                return nil, fmt.Errorf("invalid address: %w", err)
×
UNCOV
1299
        }
×
1300

1301
        if !addr.IsForNet(params) {
3✔
UNCOV
1302
                return nil, fmt.Errorf("invalid address: %v is not a %s "+
×
UNCOV
1303
                        "address", addr, params.Name)
×
UNCOV
1304
        }
×
1305

1306
        return txscript.PayToAddrScript(addr)
3✔
1307
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc