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

lightningnetwork / lnd / 13536249039

26 Feb 2025 03:42AM UTC coverage: 57.462% (-1.4%) from 58.835%
13536249039

Pull #8453

github

Roasbeef
peer: update chooseDeliveryScript to gen script if needed

In this commit, we update `chooseDeliveryScript` to generate a new
script if needed. This allows us to fold in a few other lines that
always followed this function into this expanded function.

The tests have been updated accordingly.
Pull Request #8453: [4/4] - multi: integrate new rbf coop close FSM into the existing peer flow

275 of 1318 new or added lines in 22 files covered. (20.86%)

19521 existing lines in 257 files now uncovered.

103858 of 180741 relevant lines covered (57.46%)

24750.23 hits per line

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

73.15
/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
        // FeeEstimator is used to estimate the absolute starting co-op close
150
        // fee.
151
        FeeEstimator CoopFeeEstimator
152

153
        // AuxCloser is an optional interface that can be used to modify the
154
        // way the co-op close process proceeds.
155
        AuxCloser fn.Option[AuxChanCloser]
156
}
157

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

166
        // cfg holds the configuration for this ChanCloser instance.
167
        cfg ChanCloseCfg
168

169
        // chanPoint is the full channel point of the target channel.
170
        chanPoint wire.OutPoint
171

172
        // cid is the full channel ID of the target channel.
173
        cid lnwire.ChannelID
174

175
        // negotiationHeight is the height that the fee negotiation begun at.
176
        negotiationHeight uint32
177

178
        // closingTx is the final, fully signed closing transaction. This will
179
        // only be populated once the state machine shifts to the closeFinished
180
        // state.
181
        closingTx *wire.MsgTx
182

183
        // idealFeeSat is the ideal fee that the state machine should initially
184
        // offer when starting negotiation. This will be used as a baseline.
185
        idealFeeSat btcutil.Amount
186

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

192
        // idealFeeRate is our ideal fee rate.
193
        idealFeeRate chainfee.SatPerKWeight
194

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

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

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

216
        // localDeliveryScript is the script that we'll send our settled channel
217
        // funds to.
218
        localDeliveryScript []byte
219

220
        // localInternalKey is the local delivery address Taproot internal key,
221
        // if the local delivery script is a P2TR output.
222
        localInternalKey fn.Option[btcec.PublicKey]
223

224
        // remoteDeliveryScript is the script that we'll send the remote party's
225
        // settled channel funds to.
226
        remoteDeliveryScript []byte
227

228
        // closer is ChannelParty who initiated the coop close
229
        closer lntypes.ChannelParty
230

231
        // cachedClosingSigned is a cached copy of a received ClosingSigned that
232
        // we use to handle a specific race condition caused by the independent
233
        // message processing queues.
234
        cachedClosingSigned fn.Option[lnwire.ClosingSigned]
235

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

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

246
        // auxOutputs are the optional additional outputs that might be added to
247
        // the closing transaction.
248
        auxOutputs fn.Option[AuxCloseOutputs]
249
}
250

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

16✔
257
        var weightEstimator input.TxWeightEstimator
16✔
258

16✔
259
        if chanType.IsTaproot() {
16✔
260
                weightEstimator.AddWitnessInput(
×
261
                        input.TaprootSignatureWitnessSize,
×
262
                )
×
263
        } else {
16✔
264
                weightEstimator.AddWitnessInput(input.MultiSigWitnessSize)
16✔
265
        }
16✔
266

267
        // One of these outputs might be dust, so we'll skip adding it to our
268
        // mock transaction, so the fees are more accurate.
269
        if localOutput != nil {
32✔
270
                weightEstimator.AddTxOutput(localOutput)
16✔
271
        }
16✔
272
        if remoteOutput != nil {
32✔
273
                weightEstimator.AddTxOutput(remoteOutput)
16✔
274
        }
16✔
275

276
        totalWeight := weightEstimator.Weight()
16✔
277

16✔
278
        return idealFeeRate.FeeForWeight(totalWeight)
16✔
279
}
280

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

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

16✔
294
        return calcCoopCloseFee(chanType, localTxOut, remoteTxOut, idealFeeRate)
16✔
295
}
16✔
296

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

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

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

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

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

359
        // TODO(ziggie): Make sure the ideal fee is not higher than the max fee.
360
        // Either error out or cap the ideal fee at the max fee.
361

362
        chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) "+
11✔
363
                "is: %v sat (max_fee=%v sat)", c.cfg.Channel.ChannelPoint(),
11✔
364
                int64(c.idealFeeSat), int64(c.maxFee))
11✔
365
}
366

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

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

390
                shutdownCustomRecords.WhenSome(func(cr lnwire.CustomRecords) {
×
391
                        shutdown.CustomRecords = cr
×
392
                })
×
393

394
                return nil
×
395
        })
396
        if err != nil {
11✔
397
                return nil, err
×
398
        }
×
399

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

408
                shutdown.ShutdownNonce = lnwire.SomeShutdownNonce(
2✔
409
                        firstClosingNonce.PubNonce,
2✔
410
                )
2✔
411

2✔
412
                chancloserLog.Infof("Initiating shutdown w/ nonce: %v",
2✔
413
                        spew.Sdump(firstClosingNonce.PubNonce))
2✔
414
        }
415

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

424
        chancloserLog.Infof("ChannelPoint(%v): sending shutdown message",
11✔
425
                c.chanPoint)
11✔
426

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

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

11✔
449
        return shutdown, nil
11✔
450
}
451

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

463
        chancloserLog.Infof("ChannelPoint(%v): initiating shutdown", c.chanPoint)
7✔
464

7✔
465
        shutdownMsg, err := c.initChanShutdown()
7✔
466
        if err != nil {
7✔
467
                return nil, err
×
468
        }
×
469

470
        // With the opening steps complete, we'll transition into the
471
        // closeShutdownInitiated state. In this state, we'll wait until the
472
        // other party sends their version of the shutdown message.
473
        c.state = closeShutdownInitiated
7✔
474

7✔
475
        // Finally, we'll return the shutdown message to the caller so it can
7✔
476
        // send it to the remote peer.
7✔
477
        return shutdownMsg, nil
7✔
478
}
479

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

491
        return c.closingTx, nil
11✔
492
}
493

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

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

513
// NegotiationHeight returns the negotiation height.
514
func (c *ChanCloser) NegotiationHeight() uint32 {
4✔
515
        return c.negotiationHeight
4✔
516
}
4✔
517

518
// LocalCloseOutput returns the local close output.
519
func (c *ChanCloser) LocalCloseOutput() fn.Option[CloseOutput] {
4✔
520
        return c.localCloseOutput
4✔
521
}
4✔
522

523
// RemoteCloseOutput returns the remote close output.
524
func (c *ChanCloser) RemoteCloseOutput() fn.Option[CloseOutput] {
4✔
525
        return c.remoteCloseOutput
4✔
526
}
4✔
527

528
// AuxOutputs returns optional extra outputs.
529
func (c *ChanCloser) AuxOutputs() fn.Option[AuxCloseOutputs] {
4✔
530
        return c.auxOutputs
4✔
531
}
4✔
532

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

32✔
544
        // Either way, we'll make sure that the script passed meets our
32✔
545
        // standards. The upfrontScript should have already been checked at an
32✔
546
        // earlier stage, but we'll repeat the check here for defense in depth.
32✔
547
        if len(upfrontScript) != 0 {
36✔
548
                if !lnwallet.ValidateUpfrontShutdown(upfrontScript, netParams) {
4✔
549
                        return ErrInvalidShutdownScript
×
550
                }
×
551
        }
552
        if len(peerScript) != 0 {
61✔
553
                if !lnwallet.ValidateUpfrontShutdown(peerScript, netParams) {
30✔
554
                        return ErrInvalidShutdownScript
1✔
555
                }
1✔
556
        }
557

558
        // If no upfront shutdown script was set, return early because we do
559
        // not need to enforce closure to a specific script.
560
        if len(upfrontScript) == 0 {
58✔
561
                return nil
27✔
562
        }
27✔
563

564
        // If an upfront shutdown script was provided, disconnect from the peer, as
565
        // per BOLT 2, and return an error.
566
        if !bytes.Equal(upfrontScript, peerScript) {
6✔
567
                chancloserLog.Warnf("peer's script: %x does not match upfront "+
2✔
568
                        "shutdown script: %x", peerScript, upfrontScript)
2✔
569

2✔
570
                return ErrUpfrontShutdownScriptMismatch
2✔
571
        }
2✔
572

573
        return nil
2✔
574
}
575

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

7✔
584
        noShutdown := fn.None[lnwire.Shutdown]()
7✔
585

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

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

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

632
                // Once we have checked that the other party has not violated
633
                // option upfront shutdown we set their preference for delivery
634
                // address. We'll use this when we craft the closure
635
                // transaction.
636
                c.remoteDeliveryScript = msg.Address
4✔
637

4✔
638
                // We'll generate a shutdown message of our own to send across
4✔
639
                // the wire.
4✔
640
                localShutdown, err := c.initChanShutdown()
4✔
641
                if err != nil {
4✔
642
                        return noShutdown, err
×
643
                }
×
644

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

656
                        c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{
1✔
657
                                PubNonce: shutdownNonce,
1✔
658
                        })
1✔
659
                }
660

661
                chancloserLog.Infof("ChannelPoint(%v): responding to shutdown",
4✔
662
                        c.chanPoint)
4✔
663

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

4✔
670
                return fn.Some(*localShutdown), err
4✔
671

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

682
                // Now that we know this is a valid shutdown message and
683
                // address, we'll record their preferred delivery closing
684
                // script.
685
                c.remoteDeliveryScript = msg.Address
3✔
686

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

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

703
                        c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{
1✔
704
                                PubNonce: shutdownNonce,
1✔
705
                        })
1✔
706
                }
707

708
                chancloserLog.Infof("ChannelPoint(%v): shutdown response "+
3✔
709
                        "received, entering fee negotiation", c.chanPoint)
3✔
710

3✔
711
                return noShutdown, nil
3✔
712

713
        default:
×
714
                // Otherwise we are not in a state where we can accept this
×
715
                // message.
×
716
                return noShutdown, ErrInvalidState
×
717
        }
718
}
719

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

7✔
730
        noClosingSigned := fn.None[lnwire.ClosingSigned]()
7✔
731

7✔
732
        switch c.state {
7✔
733
        case closeAwaitingFlush:
7✔
734
                // Now that we know their desired delivery script, we can
7✔
735
                // compute what our max/ideal fee will be.
7✔
736
                c.initFeeBaseline()
7✔
737

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

750
                // At this point, we can now start the fee negotiation state, by
751
                // constructing and sending our initial signature for what we
752
                // think the closing transaction should look like.
753
                c.state = closeFeeNegotiation
7✔
754

7✔
755
                if !c.cfg.Channel.IsInitiator() {
8✔
756
                        // By default this means we do nothing, but we do want
1✔
757
                        // to check if we have a cached remote offer to process.
1✔
758
                        // If we do, we'll process it here.
1✔
759
                        res := noClosingSigned
1✔
760
                        err = nil
1✔
761
                        c.cachedClosingSigned.WhenSome(
1✔
762
                                func(cs lnwire.ClosingSigned) {
1✔
UNCOV
763
                                        res, err = c.ReceiveClosingSigned(cs)
×
UNCOV
764
                                },
×
765
                        )
766

767
                        return res, err
1✔
768
                }
769

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

779
                return fn.Some(*closingSigned), nil
6✔
780

781
        default:
×
782
                return noClosingSigned, ErrInvalidState
×
783
        }
784
}
785

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

14✔
792
        noClosing := fn.None[lnwire.ClosingSigned]()
14✔
793

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

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

812
                isInitiator := c.cfg.Channel.IsInitiator()
13✔
813

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

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

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

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

852
                // If we're the initiator of the taproot channel, and we had
853
                // our fee echo'd back, then it's all good, and we can proceed
854
                // with final broadcast.
855
                case isTaproot && isInitiator && feeMatchesOffer:
1✔
856
                        break
1✔
857

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

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

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

4✔
894
                                return fn.Some(*closeSigned), nil
4✔
895
                        }
4✔
896
                }
897

898
                chancloserLog.Infof("ChannelPoint(%v) fee of %v accepted, "+
6✔
899
                        "ending negotiation", c.chanPoint, remoteProposedFee)
6✔
900

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

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

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

961
                closeTx, _, err := c.cfg.Channel.CompleteCooperativeClose(
6✔
962
                        localSig, remoteSig, c.localDeliveryScript,
6✔
963
                        c.remoteDeliveryScript, remoteProposedFee, closeOpts...,
6✔
964
                )
6✔
965
                if err != nil {
6✔
966
                        return noClosing, err
×
967
                }
×
968
                c.closingTx = closeTx
6✔
969

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

×
990
                                return aux.FinalizeClose(desc, closeTx)
×
991
                        },
×
992
                )
993
                if err != nil {
6✔
994
                        return noClosing, err
×
995
                }
×
996

997
                // Before publishing the closing tx, we persist it to the
998
                // database, such that it can be republished if something goes
999
                // wrong.
1000
                err = c.cfg.Channel.MarkCoopBroadcasted(
6✔
1001
                        closeTx, c.closer,
6✔
1002
                )
6✔
1003
                if err != nil {
6✔
UNCOV
1004
                        return noClosing, err
×
UNCOV
1005
                }
×
1006

1007
                // With the closing transaction crafted, we'll now broadcast it
1008
                // to the network.
1009
                chancloserLog.Infof("Broadcasting cooperative close tx: %v",
6✔
1010
                        lnutils.SpewLogClosure(closeTx))
6✔
1011

6✔
1012
                // Create a close channel label.
6✔
1013
                chanID := c.cfg.Channel.ShortChanID()
6✔
1014
                closeLabel := labels.MakeLabel(
6✔
1015
                        labels.LabelTypeChannelClose, &chanID,
6✔
1016
                )
6✔
1017

6✔
1018
                if err := c.cfg.BroadcastTx(closeTx, closeLabel); err != nil {
6✔
1019
                        return noClosing, err
×
1020
                }
×
1021

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

6✔
1030
                return fn.Some(*matchingOffer), nil
6✔
1031

1032
        // If we received a message while in the closeFinished state, then this
1033
        // should only be the remote party echoing the last ClosingSigned
1034
        // message that we agreed on.
1035
        case closeFinished:
1✔
1036

1✔
1037
                // There's no more to do as both sides should have already
1✔
1038
                // broadcast the closing transaction at this state.
1✔
1039
                return noClosing, nil
1✔
1040

1041
        default:
×
1042
                return noClosing, ErrInvalidState
×
1043
        }
1044
}
1045

1046
// auxCloseOutputs returns any additional outputs that should be used when
1047
// closing the channel.
1048
func (c *ChanCloser) auxCloseOutputs(
1049
        closeFee btcutil.Amount) (fn.Option[AuxCloseOutputs], error) {
18✔
1050

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

1072
                closeOuts = outs
×
1073

×
1074
                return nil
×
1075
        })
1076
        if err != nil {
18✔
1077
                return closeOuts, err
×
1078
        }
×
1079

1080
        return closeOuts, nil
18✔
1081
}
1082

1083
// proposeCloseSigned attempts to propose a new signature for the closing
1084
// transaction for a channel based on the prior fee negotiations and our
1085
// current compromise fee.
1086
func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (
1087
        *lnwire.ClosingSigned, error) {
12✔
1088

12✔
1089
        var (
12✔
1090
                closeOpts []lnwallet.ChanCloseOpt
12✔
1091
                err       error
12✔
1092
        )
12✔
1093

12✔
1094
        // If this is a taproot channel, then we'll include the musig session
12✔
1095
        // generated for the next co-op close negotiation round.
12✔
1096
        if c.cfg.Channel.ChanType().IsTaproot() {
14✔
1097
                closeOpts, err = c.cfg.MusigSession.ProposalClosingOpts()
2✔
1098
                if err != nil {
2✔
1099
                        return nil, err
×
1100
                }
×
1101
        }
1102

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

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

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

1145
                partialSig = musig.ToWireSig()
2✔
1146
        } else {
10✔
1147
                parsedSig, err = lnwire.NewSigFromSignature(rawSig)
10✔
1148
                if err != nil {
11✔
1149
                        return nil, err
1✔
1150
                }
1✔
1151
        }
1152

1153
        c.lastFeeProposal = fee
11✔
1154

11✔
1155
        chancloserLog.Infof("ChannelPoint(%v): proposing fee of %v sat to "+
11✔
1156
                "close chan", c.chanPoint, int64(fee))
11✔
1157

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

11✔
1163
        // For musig2 channels, the main sig is blank, and instead we'll send
11✔
1164
        // over a partial signature which'll be combined once our offer is
11✔
1165
        // accepted.
11✔
1166
        if partialSig != nil {
13✔
1167
                closeSignedMsg.PartialSig = lnwire.SomePartialSig(
2✔
1168
                        partialSig.PartialSig,
2✔
1169
                )
2✔
1170
        }
2✔
1171

1172
        // We'll also save this close signed, in the case that the remote party
1173
        // accepts our offer. This way, we don't have to re-sign.
1174
        c.priorFeeOffers[fee] = closeSignedMsg
11✔
1175

11✔
1176
        return closeSignedMsg, nil
11✔
1177
}
1178

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

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

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

1207
        // Otherwise, we'll *decrease* our fee by 10%.
1208
        return fee - ((fee * 1) / 10)
×
1209
}
1210

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

6✔
1217
        // TODO(roasbeef): take in number of rounds as well?
6✔
1218

6✔
1219
        chancloserLog.Infof("ChannelPoint(%v): computing fee compromise, "+
6✔
1220
                "ideal=%v, last_sent=%v, remote_offer=%v", chanPoint,
6✔
1221
                int64(ourIdealFee), int64(lastSentFee), int64(remoteFee))
6✔
1222

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

1232
        // If the last fee we sent, is equal to the fee the remote party is
1233
        // offering, then we can simply return this fee as the negotiation is
1234
        // over.
1235
        case remoteFee == lastSentFee:
×
1236
                return lastSentFee
×
1237

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

×
1249
                        return remoteFee
×
1250
                }
×
1251

1252
                // Otherwise, we'll ratchet the fee *down* using our current
1253
                // algorithm.
1254
                return ratchetFee(lastSentFee, false)
×
1255

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

×
1267
                        return remoteFee
×
1268
                }
×
1269

1270
                // Otherwise, we'll ratchet the fee up using our current
1271
                // algorithm.
1272
                return ratchetFee(lastSentFee, true)
6✔
1273

1274
        default:
×
1275
                // TODO(roasbeef): fail if their fee isn't in expected range
×
1276
                return remoteFee
×
1277
        }
1278
}
1279

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

15✔
1286
        if len(address) == 0 {
22✔
1287
                return nil, nil
7✔
1288
        }
7✔
1289

1290
        addr, err := btcutil.DecodeAddress(
8✔
1291
                address, params,
8✔
1292
        )
8✔
1293
        if err != nil {
10✔
1294
                return nil, fmt.Errorf("invalid address: %w", err)
2✔
1295
        }
2✔
1296

1297
        if !addr.IsForNet(params) {
7✔
1298
                return nil, fmt.Errorf("invalid address: %v is not a %s "+
1✔
1299
                        "address", addr, params.Name)
1✔
1300
        }
1✔
1301

1302
        return txscript.PayToAddrScript(addr)
5✔
1303
}
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