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

lightningnetwork / lnd / 13541816950

26 Feb 2025 10:30AM UTC coverage: 58.836% (+0.02%) from 58.815%
13541816950

Pull #9550

github

ellemouton
graph/db: move various cache write calls to ChannelGraph

Here, we move the graph cache writes for AddLightningNode,
DeleteLightningNode, AddChannelEdge and MarkEdgeLive to the
ChannelGraph. Since these are writes, the cache is only updated if the
DB write is successful.
Pull Request #9550: graph: extract cache from CRUD [3]

73 of 85 new or added lines in 1 file covered. (85.88%)

288 existing lines in 17 files now uncovered.

136413 of 231851 relevant lines covered (58.84%)

19233.77 hits per line

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

74.79
/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 {
19✔
260

19✔
261
        var weightEstimator input.TxWeightEstimator
19✔
262

19✔
263
        if chanType.IsTaproot() {
19✔
264
                weightEstimator.AddWitnessInput(
×
265
                        input.TaprootSignatureWitnessSize,
×
266
                )
×
267
        } else {
19✔
268
                weightEstimator.AddWitnessInput(input.MultiSigWitnessSize)
19✔
269
        }
19✔
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 {
38✔
274
                weightEstimator.AddTxOutput(localOutput)
19✔
275
        }
19✔
276
        if remoteOutput != nil {
38✔
277
                weightEstimator.AddTxOutput(remoteOutput)
19✔
278
        }
19✔
279

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

19✔
282
        return idealFeeRate.FeeForWeight(totalWeight)
19✔
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 {
19✔
297

19✔
298
        return calcCoopCloseFee(chanType, localTxOut, remoteTxOut, idealFeeRate)
19✔
299
}
19✔
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 {
18✔
308

18✔
309
        chanPoint := cfg.Channel.ChannelPoint()
18✔
310
        cid := lnwire.NewChanIDFromOutPoint(chanPoint)
18✔
311
        return &ChanCloser{
18✔
312
                closeReq:            closeReq,
18✔
313
                state:               closeIdle,
18✔
314
                chanPoint:           chanPoint,
18✔
315
                cid:                 cid,
18✔
316
                cfg:                 cfg,
18✔
317
                negotiationHeight:   negotiationHeight,
18✔
318
                idealFeeRate:        idealFeePerKw,
18✔
319
                localInternalKey:    deliveryScript.InternalKey,
18✔
320
                localDeliveryScript: deliveryScript.DeliveryAddress,
18✔
321
                priorFeeOffers: make(
18✔
322
                        map[btcutil.Amount]*lnwire.ClosingSigned,
18✔
323
                ),
18✔
324
                closer: closer,
18✔
325
        }
18✔
326
}
18✔
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() {
14✔
331
        // Depending on if a balance ends up being dust or not, we'll pass a
14✔
332
        // nil TxOut into the EstimateFee call which can handle it.
14✔
333
        var localTxOut, remoteTxOut *wire.TxOut
14✔
334
        if isDust, _ := c.cfg.Channel.LocalBalanceDust(); !isDust {
28✔
335
                localTxOut = &wire.TxOut{
14✔
336
                        PkScript: c.localDeliveryScript,
14✔
337
                        Value:    0,
14✔
338
                }
14✔
339
        }
14✔
340
        if isDust, _ := c.cfg.Channel.RemoteBalanceDust(); !isDust {
28✔
341
                remoteTxOut = &wire.TxOut{
14✔
342
                        PkScript: c.remoteDeliveryScript,
14✔
343
                        Value:    0,
14✔
344
                }
14✔
345
        }
14✔
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(
14✔
350
                0, localTxOut, remoteTxOut, c.idealFeeRate,
14✔
351
        )
14✔
352

14✔
353
        // When we're the initiator, we'll want to also factor in the highest
14✔
354
        // fee we want to pay. This'll either be 3x the ideal fee, or the
14✔
355
        // specified explicit max fee.
14✔
356
        c.maxFee = c.idealFeeSat * defaultMaxFeeMultiplier
14✔
357
        if c.cfg.MaxFee > 0 {
22✔
358
                c.maxFee = c.cfg.FeeEstimator.EstimateFee(
8✔
359
                        0, localTxOut, remoteTxOut, c.cfg.MaxFee,
8✔
360
                )
8✔
361
        }
8✔
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) "+
14✔
367
                "is: %v sat (max_fee=%v sat)", c.cfg.Channel.ChannelPoint(),
14✔
368
                int64(c.idealFeeSat), int64(c.maxFee))
14✔
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) {
14✔
374
        // With both items constructed we'll now send the shutdown message for
14✔
375
        // this particular channel, advertising a shutdown request to our
14✔
376
        // desired closing script.
14✔
377
        shutdown := lnwire.NewShutdown(c.cid, c.localDeliveryScript)
14✔
378

14✔
379
        // At this point, we'll check to see if we have any custom records to
14✔
380
        // add to the shutdown message.
14✔
381
        err := fn.MapOptionZ(c.cfg.AuxCloser, func(a AuxChanCloser) error {
14✔
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 {
14✔
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() {
19✔
407
                firstClosingNonce, err := c.cfg.MusigSession.ClosingNonce()
5✔
408
                if err != nil {
5✔
409
                        return nil, err
×
410
                }
×
411

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

5✔
416
                chancloserLog.Infof("Initiating shutdown w/ nonce: %v",
5✔
417
                        spew.Sdump(firstClosingNonce.PubNonce))
5✔
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 {
23✔
424
                chancloserLog.Warnf("Unable to disable channel %v on close: %v",
9✔
425
                        c.chanPoint, err)
9✔
426
        }
9✔
427

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

14✔
431
        // At this point, we persist any relevant info regarding the Shutdown
14✔
432
        // message we are about to send in order to ensure that if a
14✔
433
        // re-establish occurs then we will re-send the same Shutdown message.
14✔
434
        shutdownInfo := channeldb.NewShutdownInfo(
14✔
435
                c.localDeliveryScript, c.closer.IsLocal(),
14✔
436
        )
14✔
437
        err = c.cfg.Channel.MarkShutdownSent(shutdownInfo)
14✔
438
        if err != nil {
14✔
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()
14✔
445
        localBalance, _ := c.cfg.Channel.CommitBalances()
14✔
446
        c.localCloseOutput = fn.Some(CloseOutput{
14✔
447
                Amt:             localBalance,
14✔
448
                DustLimit:       dustAmt,
14✔
449
                PkScript:        c.localDeliveryScript,
14✔
450
                ShutdownRecords: shutdown.CustomRecords,
14✔
451
        })
14✔
452

14✔
453
        return shutdown, nil
14✔
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) {
10✔
461
        // If we attempt to shutdown the channel for the first time, and we're not
10✔
462
        // in the closeIdle state, then the caller made an error.
10✔
463
        if c.state != closeIdle {
10✔
464
                return nil, ErrChanAlreadyClosing
×
465
        }
×
466

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

10✔
469
        shutdownMsg, err := c.initChanShutdown()
10✔
470
        if err != nil {
10✔
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
10✔
478

10✔
479
        // Finally, we'll return the shutdown message to the caller so it can
10✔
480
        // send it to the remote peer.
10✔
481
        return shutdownMsg, nil
10✔
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) {
24✔
489
        // If the state machine hasn't finished closing the channel, then we'll
24✔
490
        // return an error as we haven't yet computed the closing tx.
24✔
491
        if c.state != closeFinished {
37✔
492
                return nil, ErrChanCloseNotFinished
13✔
493
        }
13✔
494

495
        return c.closingTx, nil
14✔
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 {
7✔
504
        return c.closeReq
7✔
505
}
7✔
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 {
7✔
513
        // TODO(roasbeef): remove this
7✔
514
        return c.cfg.Channel.(*lnwallet.LightningChannel)
7✔
515
}
7✔
516

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

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

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

532
// AuxOutputs returns optional extra outputs.
533
func (c *ChanCloser) AuxOutputs() fn.Option[AuxCloseOutputs] {
7✔
534
        return c.auxOutputs
7✔
535
}
7✔
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 {
35✔
547

35✔
548
        // Either way, we'll make sure that the script passed meets our
35✔
549
        // standards. The upfrontScript should have already been checked at an
35✔
550
        // earlier stage, but we'll repeat the check here for defense in depth.
35✔
551
        if len(upfrontScript) != 0 {
42✔
552
                if !lnwallet.ValidateUpfrontShutdown(upfrontScript, netParams) {
7✔
553
                        return ErrInvalidShutdownScript
×
554
                }
×
555
        }
556
        if len(peerScript) != 0 {
67✔
557
                if !lnwallet.ValidateUpfrontShutdown(peerScript, netParams) {
33✔
558
                        return ErrInvalidShutdownScript
1✔
559
                }
1✔
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 {
64✔
565
                return nil
30✔
566
        }
30✔
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) {
9✔
571
                chancloserLog.Warnf("peer's script: %x does not match upfront "+
2✔
572
                        "shutdown script: %x", peerScript, upfrontScript)
2✔
573

2✔
574
                return ErrUpfrontShutdownScriptMismatch
2✔
575
        }
2✔
576

577
        return nil
5✔
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) {
10✔
587

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

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

10✔
601
        switch c.state {
10✔
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:
7✔
606
                // As we're the responder to this shutdown (the other party
7✔
607
                // wants to close), we'll check if this is a frozen channel or
7✔
608
                // not. If the channel is frozen and we were not also the
7✔
609
                // initiator of the channel opening, then we'll deny their close
7✔
610
                // attempt.
7✔
611
                chanInitiator := c.cfg.Channel.IsInitiator()
7✔
612
                if !chanInitiator {
11✔
613
                        absoluteThawHeight, err :=
4✔
614
                                c.cfg.Channel.AbsoluteThawHeight()
4✔
615
                        if err != nil {
4✔
616
                                return noShutdown, err
×
617
                        }
×
618
                        if c.negotiationHeight < absoluteThawHeight {
4✔
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(
7✔
630
                        c.cfg.Channel.RemoteUpfrontShutdownScript(),
7✔
631
                        msg.Address, c.cfg.ChainParams,
7✔
632
                ); err != nil {
7✔
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
7✔
641

7✔
642
                // We'll generate a shutdown message of our own to send across
7✔
643
                // the wire.
7✔
644
                localShutdown, err := c.initChanShutdown()
7✔
645
                if err != nil {
7✔
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() {
11✔
653
                        shutdownNonce, err := msg.ShutdownNonce.UnwrapOrErrV(
4✔
654
                                errNoShutdownNonce,
4✔
655
                        )
4✔
656
                        if err != nil {
4✔
657
                                return noShutdown, err
×
658
                        }
×
659

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

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

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

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

676
        case closeShutdownInitiated:
6✔
677
                // If the remote node opened the channel with option upfront
6✔
678
                // shutdown script, check that the script they provided matches.
6✔
679
                if err := validateShutdownScript(
6✔
680
                        c.cfg.Channel.RemoteUpfrontShutdownScript(),
6✔
681
                        msg.Address, c.cfg.ChainParams,
6✔
682
                ); err != nil {
6✔
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
6✔
690

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

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

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

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

6✔
715
                return noShutdown, nil
6✔
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) {
10✔
733

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

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

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

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

771
                        return res, err
4✔
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)
9✔
777
                if err != nil {
9✔
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
9✔
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) {
17✔
795

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

17✔
798
        switch c.state {
17✔
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:
16✔
807
                // If this is a taproot channel, then it MUST have a partial
16✔
808
                // signature set at this point.
16✔
809
                isTaproot := c.cfg.Channel.ChanType().IsTaproot()
16✔
810
                if isTaproot && msg.PartialSig.IsNone() {
16✔
811
                        return noClosing,
×
812
                                fmt.Errorf("partial sig not set " +
×
813
                                        "for taproot chan")
×
814
                }
×
815

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

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

16✔
824
                _, feeMatchesOffer := c.priorFeeOffers[remoteProposedFee]
16✔
825
                switch {
16✔
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:
4✔
831
                        chancloserLog.Infof("ChannelPoint(%v) accepting "+
4✔
832
                                "initiator fee of %v", c.chanPoint,
4✔
833
                                remoteProposedFee)
4✔
834

4✔
835
                        // To auto-accept the initiators proposal, we'll just
4✔
836
                        // send back a signature w/ the same offer. We don't
4✔
837
                        // send the message here, as we can drop down and
4✔
838
                        // finalize the closure and broadcast, then echo back
4✔
839
                        // to Alice the final signature.
4✔
840
                        _, err := c.proposeCloseSigned(remoteProposedFee)
4✔
841
                        if err != nil {
4✔
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.
849
                case isTaproot && isInitiator && !feeMatchesOffer:
1✔
850
                        return noClosing,
1✔
851
                                fmt.Errorf("fee rate for "+
1✔
852
                                        "taproot channels was not accepted: "+
1✔
853
                                        "sent %v, got %v",
1✔
854
                                        c.idealFeeSat, remoteProposedFee)
1✔
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:
4✔
860
                        break
4✔
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:
9✔
866
                        // We'll now attempt to ratchet towards a fee deemed
9✔
867
                        // acceptable by both parties, factoring in our ideal
9✔
868
                        // fee rate, and the last proposed fee by both sides.
9✔
869
                        proposal := calcCompromiseFee(
9✔
870
                                c.chanPoint, c.idealFeeSat, c.lastFeeProposal,
9✔
871
                                remoteProposedFee,
9✔
872
                        )
9✔
873
                        if c.cfg.Channel.IsInitiator() && proposal > c.maxFee {
10✔
874
                                return noClosing, fmt.Errorf(
1✔
875
                                        "%w: %v > %v",
1✔
876
                                        ErrProposalExceedsMaxFee,
1✔
877
                                        proposal, c.maxFee)
1✔
878
                        }
1✔
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)
8✔
885
                        if err != nil {
9✔
886
                                return noClosing, fmt.Errorf("unable to sign "+
1✔
887
                                        "new co op close offer: %w", err)
1✔
888
                        }
1✔
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 {
11✔
894
                                chancloserLog.Debugf("ChannelPoint(%v): close "+
4✔
895
                                        "tx fee disagreement, continuing "+
4✔
896
                                        "negotiation", c.chanPoint)
4✔
897

4✔
898
                                return fn.Some(*closeSigned), nil
4✔
899
                        }
4✔
900
                }
901

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

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

928
                        muSession := c.cfg.MusigSession
5✔
929
                        localSig, remoteSig, closeOpts, err = muSession.CombineClosingOpts( //nolint:ll
5✔
930
                                localWireSig, remoteWireSig,
5✔
931
                        )
5✔
932
                        if err != nil {
5✔
933
                                return noClosing, err
×
934
                        }
×
935
                } else {
7✔
936
                        localSig, err = matchingSig.Signature.ToSignature()
7✔
937
                        if err != nil {
7✔
938
                                return noClosing, err
×
939
                        }
×
940
                        remoteSig, err = msg.Signature.ToSignature()
7✔
941
                        if err != nil {
7✔
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)
9✔
949
                if err != nil {
9✔
950
                        return noClosing, err
×
951
                }
×
952
                c.auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
9✔
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(
9✔
966
                        localSig, remoteSig, c.localDeliveryScript,
9✔
967
                        c.remoteDeliveryScript, remoteProposedFee, closeOpts...,
9✔
968
                )
9✔
969
                if err != nil {
9✔
970
                        return noClosing, err
×
971
                }
×
972
                c.closingTx = closeTx
9✔
973

9✔
974
                // If there's an aux chan closer, then we'll finalize with it
9✔
975
                // before we write to disk.
9✔
976
                err = fn.MapOptionZ(
9✔
977
                        c.cfg.AuxCloser, func(aux AuxChanCloser) error {
9✔
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 {
9✔
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(
9✔
1005
                        closeTx, c.closer,
9✔
1006
                )
9✔
1007
                if err != nil {
10✔
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",
9✔
1014
                        lnutils.SpewLogClosure(closeTx))
9✔
1015

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

9✔
1022
                if err := c.cfg.BroadcastTx(closeTx, closeLabel); err != nil {
9✔
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
9✔
1032
                matchingOffer := c.priorFeeOffers[remoteProposedFee]
9✔
1033

9✔
1034
                return fn.Some(*matchingOffer), nil
9✔
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:
4✔
1040

4✔
1041
                // There's no more to do as both sides should have already
4✔
1042
                // broadcast the closing transaction at this state.
4✔
1043
                return noClosing, nil
4✔
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) {
21✔
1054

21✔
1055
        var closeOuts fn.Option[AuxCloseOutputs]
21✔
1056
        err := fn.MapOptionZ(c.cfg.AuxCloser, func(aux AuxChanCloser) error {
21✔
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 {
21✔
1081
                return closeOuts, err
×
1082
        }
×
1083

1084
        return closeOuts, nil
21✔
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) {
15✔
1092

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

15✔
1098
        // If this is a taproot channel, then we'll include the musig session
15✔
1099
        // generated for the next co-op close negotiation round.
15✔
1100
        if c.cfg.Channel.ChanType().IsTaproot() {
20✔
1101
                closeOpts, err = c.cfg.MusigSession.ProposalClosingOpts()
5✔
1102
                if err != nil {
5✔
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)
15✔
1110
        if err != nil {
15✔
1111
                return nil, err
×
1112
        }
×
1113
        c.auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
15✔
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(
15✔
1128
                fee, c.localDeliveryScript, c.remoteDeliveryScript,
15✔
1129
                closeOpts...,
15✔
1130
        )
15✔
1131
        if err != nil {
15✔
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 (
15✔
1139
                parsedSig  lnwire.Sig
15✔
1140
                partialSig *lnwire.PartialSigWithNonce
15✔
1141
        )
15✔
1142
        if c.cfg.Channel.ChanType().IsTaproot() {
20✔
1143
                musig, ok := rawSig.(*lnwallet.MusigPartialSig)
5✔
1144
                if !ok {
5✔
1145
                        return nil, fmt.Errorf("expected MusigPartialSig, "+
×
1146
                                "got %T", rawSig)
×
1147
                }
×
1148

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

1157
        c.lastFeeProposal = fee
14✔
1158

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

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

14✔
1167
        // For musig2 channels, the main sig is blank, and instead we'll send
14✔
1168
        // over a partial signature which'll be combined once our offer is
14✔
1169
        // accepted.
14✔
1170
        if partialSig != nil {
19✔
1171
                closeSignedMsg.PartialSig = lnwire.SomePartialSig(
5✔
1172
                        partialSig.PartialSig,
5✔
1173
                )
5✔
1174
        }
5✔
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
14✔
1179

14✔
1180
        return closeSignedMsg, nil
14✔
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.
1187
func feeInAcceptableRange(localFee, remoteFee btcutil.Amount) bool {
6✔
1188
        // If our offer is lower than theirs, then we'll accept their offer if
6✔
1189
        // it's no more than 30% *greater* than our current offer.
6✔
1190
        if localFee < remoteFee {
12✔
1191
                acceptableRange := localFee + ((localFee * 3) / 10)
6✔
1192
                return remoteFee <= acceptableRange
6✔
1193
        }
6✔
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.
1205
func ratchetFee(fee btcutil.Amount, up bool) btcutil.Amount {
6✔
1206
        // If we need to ratchet up, then we'll increase our fee by 10%.
6✔
1207
        if up {
12✔
1208
                return fee + ((fee * 1) / 10)
6✔
1209
        }
6✔
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 {
9✔
1220

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

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

9✔
1227
        // Otherwise, we'll need to attempt to make a fee compromise if this is
9✔
1228
        // the second round, and neither side has agreed on fees.
9✔
1229
        switch {
9✔
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.
1263
        case remoteFee > lastSentFee:
6✔
1264
                // If the fee is greater, but still acceptable, then we'll just
6✔
1265
                // return this fee in order to put an end to the negotiation.
6✔
1266
                if feeInAcceptableRange(lastSentFee, remoteFee) {
6✔
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.
1276
                return ratchetFee(lastSentFee, true)
6✔
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) {
18✔
1289

18✔
1290
        if len(address) == 0 {
28✔
1291
                return nil, nil
10✔
1292
        }
10✔
1293

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

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

1306
        return txscript.PayToAddrScript(addr)
8✔
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