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

lightningnetwork / lnd / 17949158775

23 Sep 2025 02:18PM UTC coverage: 66.713% (+0.07%) from 66.647%
17949158775

Pull #10232

github

web-flow
Merge fc770ddb4 into 82f77e542
Pull Request #10232: lnwire: add missing Gossip 1.75 fields and message

491 of 568 new or added lines in 19 files covered. (86.44%)

64 existing lines in 19 files now uncovered.

136915 of 205230 relevant lines covered (66.71%)

21387.93 hits per line

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

91.66
/lnwallet/chancloser/rbf_coop_transitions.go
1
package chancloser
2

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

7
        "github.com/btcsuite/btcd/btcec/v2"
8
        "github.com/btcsuite/btcd/btcutil"
9
        "github.com/btcsuite/btcd/chaincfg"
10
        "github.com/btcsuite/btcd/mempool"
11
        "github.com/btcsuite/btcd/wire"
12
        "github.com/lightningnetwork/lnd/fn/v2"
13
        "github.com/lightningnetwork/lnd/input"
14
        "github.com/lightningnetwork/lnd/labels"
15
        "github.com/lightningnetwork/lnd/lntypes"
16
        "github.com/lightningnetwork/lnd/lnutils"
17
        "github.com/lightningnetwork/lnd/lnwallet"
18
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
19
        "github.com/lightningnetwork/lnd/lnwire"
20
        "github.com/lightningnetwork/lnd/protofsm"
21
        "github.com/lightningnetwork/lnd/tlv"
22
)
23

24
var (
25
        // ErrThawHeightNotReached is returned if the remote party tries to
26
        // close, but the thaw height hasn't been matched yet.
27
        ErrThawHeightNotReached = fmt.Errorf("thaw height not reached")
28
)
29

30
// sendShutdownEvents is a helper function that returns a set of daemon events
31
// we need to emit when we decide that we should send a shutdown message. We'll
32
// also mark the channel as borked as well, as at this point, we no longer want
33
// to continue with normal operation.
34
func sendShutdownEvents(chanID lnwire.ChannelID, chanPoint wire.OutPoint,
35
        deliveryAddr lnwire.DeliveryAddress, peerPub btcec.PublicKey,
36
        postSendEvent fn.Option[ProtocolEvent],
37
        chanState ChanStateObserver) (protofsm.DaemonEventSet, error) {
5✔
38

5✔
39
        // We'll emit a daemon event that instructs the daemon to send out a
5✔
40
        // new shutdown message to the remote peer.
5✔
41
        msgsToSend := &protofsm.SendMsgEvent[ProtocolEvent]{
5✔
42
                TargetPeer: peerPub,
5✔
43
                Msgs: []lnwire.Message{&lnwire.Shutdown{
5✔
44
                        ChannelID: chanID,
5✔
45
                        Address:   deliveryAddr,
5✔
46
                }},
5✔
47
                SendWhen: fn.Some(func() bool {
11✔
48
                        ok := chanState.NoDanglingUpdates()
6✔
49
                        if ok {
10✔
50
                                chancloserLog.Infof("ChannelPoint(%v): no "+
4✔
51
                                        "dangling updates sending shutdown "+
4✔
52
                                        "message", chanPoint)
4✔
53
                        }
4✔
54

55
                        return ok
6✔
56
                }),
57
                PostSendEvent: postSendEvent,
58
        }
59

60
        // If a close is already in process (we're in the RBF loop), then we
61
        // can skip everything below, and just send out the shutdown message.
62
        if chanState.FinalBalances().IsSome() {
8✔
63
                return protofsm.DaemonEventSet{msgsToSend}, nil
3✔
64
        }
3✔
65

66
        // Before closing, we'll attempt to send a disable update for the
67
        // channel.  We do so before closing the channel as otherwise the
68
        // current edge policy won't be retrievable from the graph.
69
        if err := chanState.DisableChannel(); err != nil {
5✔
70
                return nil, fmt.Errorf("unable to disable channel: %w", err)
×
71
        }
×
72

73
        // If we have a post-send event, then this means that we're the
74
        // responder. We'll use this fact below to update state in the DB.
75
        isInitiator := postSendEvent.IsNone()
5✔
76

5✔
77
        chancloserLog.Infof("ChannelPoint(%v): disabling outgoing adds",
5✔
78
                chanPoint)
5✔
79

5✔
80
        // As we're about to send a shutdown, we'll disable adds in the
5✔
81
        // outgoing direction.
5✔
82
        if err := chanState.DisableOutgoingAdds(); err != nil {
5✔
83
                return nil, fmt.Errorf("unable to disable outgoing "+
×
84
                        "adds: %w", err)
×
85
        }
×
86

87
        // To be able to survive a restart, we'll also write to disk
88
        // information about the shutdown we're about to send out.
89
        err := chanState.MarkShutdownSent(deliveryAddr, isInitiator)
5✔
90
        if err != nil {
5✔
91
                return nil, fmt.Errorf("unable to mark shutdown sent: %w", err)
×
92
        }
×
93

94
        chancloserLog.Debugf("ChannelPoint(%v): marking channel as borked",
5✔
95
                chanPoint)
5✔
96

5✔
97
        return protofsm.DaemonEventSet{msgsToSend}, nil
5✔
98
}
99

100
// validateShutdown is a helper function that validates that the shutdown has a
101
// proper delivery script, and can be sent based on the current thaw height of
102
// the channel.
103
func validateShutdown(chanThawHeight fn.Option[uint32],
104
        upfrontAddr fn.Option[lnwire.DeliveryAddress],
105
        msg *ShutdownReceived, chanPoint wire.OutPoint,
106
        chainParams chaincfg.Params) error {
8✔
107

8✔
108
        // If we've received a shutdown message, and we have a thaw height,
8✔
109
        // then we need to make sure that the channel can now be co-op closed.
8✔
110
        err := fn.MapOptionZ(chanThawHeight, func(thawHeight uint32) error {
12✔
111
                // If the current height is below the thaw height, then we'll
4✔
112
                // reject the shutdown message as we can't yet co-op close the
4✔
113
                // channel.
4✔
114
                if msg.BlockHeight < thawHeight {
5✔
115
                        return fmt.Errorf("%w: initiator attempting to "+
1✔
116
                                "co-op close frozen ChannelPoint(%v) "+
1✔
117
                                "(current_height=%v, thaw_height=%v)",
1✔
118
                                ErrThawHeightNotReached, chanPoint,
1✔
119
                                msg.BlockHeight, thawHeight)
1✔
120
                }
1✔
121

122
                return nil
3✔
123
        })
124
        if err != nil {
9✔
125
                return err
1✔
126
        }
1✔
127

128
        // Next, we'll verify that the remote party is sending the expected
129
        // shutdown script.
130
        return fn.MapOption(func(addr lnwire.DeliveryAddress) error {
9✔
131
                return validateShutdownScript(
2✔
132
                        addr, msg.ShutdownScript, &chainParams,
2✔
133
                )
2✔
134
        })(upfrontAddr).UnwrapOr(nil)
2✔
135
}
136

137
// ProcessEvent takes a protocol event, and implements a state transition for
138
// the state. From this state, we can receive two possible incoming events:
139
// SendShutdown and ShutdownReceived. Both of these will transition us to the
140
// ChannelFlushing state.
141
func (c *ChannelActive) ProcessEvent(event ProtocolEvent,
142
        env *Environment) (*CloseStateTransition, error) {
10✔
143

10✔
144
        switch msg := event.(type) {
10✔
145
        // If we get a confirmation, then a prior transaction we broadcasted
146
        // has confirmed, so we can move to our terminal state early.
147
        case *SpendEvent:
2✔
148
                return &CloseStateTransition{
2✔
149
                        NextState: &CloseFin{
2✔
150
                                ConfirmedTx: msg.Tx,
2✔
151
                        },
2✔
152
                }, nil
2✔
153

154
        // If we receive the SendShutdown event, then we'll send our shutdown
155
        // with a special SendPredicate, then go to the ShutdownPending where
156
        // we'll wait for the remote to send their shutdown.
157
        case *SendShutdown:
5✔
158
                // If we have an upfront shutdown addr or a delivery addr then
5✔
159
                // we'll use that. Otherwise, we'll generate a new delivery
5✔
160
                // addr.
5✔
161
                shutdownScript, err := env.LocalUpfrontShutdown.Alt(
5✔
162
                        msg.DeliveryAddr,
5✔
163
                ).UnwrapOrFuncErr(env.NewDeliveryScript)
5✔
164
                if err != nil {
6✔
165
                        return nil, err
1✔
166
                }
1✔
167

168
                // We'll emit some daemon events to send the shutdown message
169
                // and disable the channel on the network level. In this case,
170
                // we don't need a post send event as receive their shutdown is
171
                // what'll move us beyond the ShutdownPending state.
172
                daemonEvents, err := sendShutdownEvents(
4✔
173
                        env.ChanID, env.ChanPoint, shutdownScript,
4✔
174
                        env.ChanPeer, fn.None[ProtocolEvent](),
4✔
175
                        env.ChanObserver,
4✔
176
                )
4✔
177
                if err != nil {
4✔
178
                        return nil, err
×
179
                }
×
180

181
                chancloserLog.Infof("ChannelPoint(%v): sending shutdown msg, "+
4✔
182
                        "delivery_script=%x", env.ChanPoint, shutdownScript)
4✔
183

4✔
184
                // From here, we'll transition to the shutdown pending state. In
4✔
185
                // this state we await their shutdown message (self loop), then
4✔
186
                // also the flushing event.
4✔
187
                return &CloseStateTransition{
4✔
188
                        NextState: &ShutdownPending{
4✔
189
                                IdealFeeRate: fn.Some(msg.IdealFeeRate),
4✔
190
                                ShutdownScripts: ShutdownScripts{
4✔
191
                                        LocalDeliveryScript: shutdownScript,
4✔
192
                                },
4✔
193
                        },
4✔
194
                        NewEvents: fn.Some(RbfEvent{
4✔
195
                                ExternalEvents: daemonEvents,
4✔
196
                        }),
4✔
197
                }, nil
4✔
198

199
        // When we receive a shutdown from the remote party, we'll validate the
200
        // shutdown message, then transition to the ShutdownPending state. We'll
201
        // also emit similar events like the above to send out shutdown, and
202
        // also disable the channel.
203
        case *ShutdownReceived:
5✔
204
                chancloserLog.Infof("ChannelPoint(%v): received shutdown msg",
5✔
205
                        env.ChanPoint)
5✔
206

5✔
207
                // Validate that they can send the message now, and also that
5✔
208
                // they haven't violated their commitment to a prior upfront
5✔
209
                // shutdown addr.
5✔
210
                err := validateShutdown(
5✔
211
                        env.ThawHeight, env.RemoteUpfrontShutdown, msg,
5✔
212
                        env.ChanPoint, env.ChainParams,
5✔
213
                )
5✔
214
                if err != nil {
6✔
215
                        chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
1✔
216
                                "shutdown attempt: %v", env.ChanPoint, err)
1✔
217

1✔
218
                        return nil, err
1✔
219
                }
1✔
220

221
                // If we have an upfront shutdown addr we'll use that,
222
                // otherwise, we'll generate a new delivery script.
223
                shutdownAddr, err := env.LocalUpfrontShutdown.UnwrapOrFuncErr(
4✔
224
                        env.NewDeliveryScript,
4✔
225
                )
4✔
226
                if err != nil {
4✔
227
                        return nil, err
×
228
                }
×
229

230
                chancloserLog.Infof("ChannelPoint(%v): sending shutdown msg "+
4✔
231
                        "at next clean commit state", env.ChanPoint)
4✔
232

4✔
233
                // Now that we know the shutdown message is valid, we'll obtain
4✔
234
                // the set of daemon events we need to emit. We'll also specify
4✔
235
                // that once the message has actually been sent, that we
4✔
236
                // generate receive an input event of a ShutdownComplete.
4✔
237
                daemonEvents, err := sendShutdownEvents(
4✔
238
                        env.ChanID, env.ChanPoint, shutdownAddr,
4✔
239
                        env.ChanPeer,
4✔
240
                        fn.Some[ProtocolEvent](&ShutdownComplete{}),
4✔
241
                        env.ChanObserver,
4✔
242
                )
4✔
243
                if err != nil {
4✔
244
                        return nil, err
×
245
                }
×
246

247
                chancloserLog.Infof("ChannelPoint(%v): disabling incoming adds",
4✔
248
                        env.ChanPoint)
4✔
249

4✔
250
                // We just received a shutdown, so we'll disable the adds in
4✔
251
                // the outgoing direction.
4✔
252
                if err := env.ChanObserver.DisableIncomingAdds(); err != nil {
4✔
253
                        return nil, fmt.Errorf("unable to disable incoming "+
×
254
                                "adds: %w", err)
×
255
                }
×
256

257
                remoteAddr := msg.ShutdownScript
4✔
258

4✔
259
                return &CloseStateTransition{
4✔
260
                        NextState: &ShutdownPending{
4✔
261
                                ShutdownScripts: ShutdownScripts{
4✔
262
                                        LocalDeliveryScript:  shutdownAddr,
4✔
263
                                        RemoteDeliveryScript: remoteAddr,
4✔
264
                                },
4✔
265
                        },
4✔
266
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
4✔
267
                                ExternalEvents: daemonEvents,
4✔
268
                        }),
4✔
269
                }, nil
4✔
270

271
        // Any other messages in this state will result in an error, as this is
272
        // an undefined state transition.
273
        default:
1✔
274
                return nil, fmt.Errorf("%w: received %T while in ChannelActive",
1✔
275
                        ErrInvalidStateTransition, msg)
1✔
276
        }
277
}
278

279
// ProcessEvent takes a protocol event, and implements a state transition for
280
// the state. Our path to this state will determine the set of valid events. If
281
// we were the one that sent the shutdown, then we'll just wait on the
282
// ShutdownReceived event. Otherwise, we received the shutdown, and can move
283
// forward once we receive the ShutdownComplete event. Receiving
284
// ShutdownComplete means that we've sent our shutdown, as this was specified
285
// as a post send event.
286
func (s *ShutdownPending) ProcessEvent(event ProtocolEvent,
287
        env *Environment) (*CloseStateTransition, error) {
13✔
288

13✔
289
        switch msg := event.(type) {
13✔
290
        // If we get a confirmation, then a prior transaction we broadcasted
291
        // has confirmed, so we can move to our terminal state early.
292
        case *SpendEvent:
2✔
293
                return &CloseStateTransition{
2✔
294
                        NextState: &CloseFin{
2✔
295
                                ConfirmedTx: msg.Tx,
2✔
296
                        },
2✔
297
                }, nil
2✔
298

299
        // The remote party sent an offer early. We'll go to the ChannelFlushing
300
        // case, and then emit the offer as a internal event, which'll be
301
        // handled as an early offer.
302
        case *OfferReceivedEvent:
2✔
303
                chancloserLog.Infof("ChannelPoint(%v): got an early offer "+
2✔
304
                        "in ShutdownPending, emitting as external event",
2✔
305
                        env.ChanPoint)
2✔
306

2✔
307
                s.EarlyRemoteOffer = fn.Some(*msg)
2✔
308

2✔
309
                // We'll perform a noop update so we can wait for the actual
2✔
310
                // channel flushed event.
2✔
311
                return &CloseStateTransition{
2✔
312
                        NextState: s,
2✔
313
                }, nil
2✔
314

315
        // When we receive a shutdown from the remote party, we'll validate the
316
        // shutdown message, then transition to the ChannelFlushing state.
317
        case *ShutdownReceived:
6✔
318
                chancloserLog.Infof("ChannelPoint(%v): received shutdown msg",
6✔
319
                        env.ChanPoint)
6✔
320

6✔
321
                // Validate that they can send the message now, and also that
6✔
322
                // they haven't violated their commitment to a prior upfront
6✔
323
                // shutdown addr.
6✔
324
                err := validateShutdown(
6✔
325
                        env.ThawHeight, env.RemoteUpfrontShutdown, msg,
6✔
326
                        env.ChanPoint, env.ChainParams,
6✔
327
                )
6✔
328
                if err != nil {
7✔
329
                        chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
1✔
330
                                "shutdown attempt: %v", env.ChanPoint, err)
1✔
331

1✔
332
                        return nil, err
1✔
333
                }
1✔
334

335
                // If the channel is *already* flushed, and the close is
336
                // go straight into negotiation, as this is the RBF loop.
337
                // already in progress, then we can skip the flushing state and
338
                var eventsToEmit []ProtocolEvent
5✔
339
                finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
5✔
340
                        unknownBalance,
5✔
341
                )
5✔
342
                if finalBalances != unknownBalance {
8✔
343
                        channelFlushed := ProtocolEvent(&ChannelFlushed{
3✔
344
                                ShutdownBalances: finalBalances,
3✔
345
                        })
3✔
346
                        eventsToEmit = append(eventsToEmit, channelFlushed)
3✔
347
                }
3✔
348

349
                chancloserLog.Infof("ChannelPoint(%v): disabling incoming adds",
5✔
350
                        env.ChanPoint)
5✔
351

5✔
352
                // We just received a shutdown, so we'll disable the adds in
5✔
353
                // the outgoing direction.
5✔
354
                if err := env.ChanObserver.DisableIncomingAdds(); err != nil {
5✔
355
                        return nil, fmt.Errorf("unable to disable incoming "+
×
356
                                "adds: %w", err)
×
357
                }
×
358

359
                chancloserLog.Infof("ChannelPoint(%v): waiting for channel to "+
5✔
360
                        "be flushed...", env.ChanPoint)
5✔
361

5✔
362
                // If we received a remote offer early from the remote party,
5✔
363
                // then we'll add that to the set of internal events to emit.
5✔
364
                s.EarlyRemoteOffer.WhenSome(func(offer OfferReceivedEvent) {
6✔
365
                        eventsToEmit = append(eventsToEmit, &offer)
1✔
366
                })
1✔
367

368
                var newEvents fn.Option[RbfEvent]
5✔
369
                if len(eventsToEmit) > 0 {
9✔
370
                        newEvents = fn.Some(RbfEvent{
4✔
371
                                InternalEvent: eventsToEmit,
4✔
372
                        })
4✔
373
                }
4✔
374

375
                // We transition to the ChannelFlushing state, where we await
376
                // the ChannelFlushed event.
377
                return &CloseStateTransition{
5✔
378
                        NextState: &ChannelFlushing{
5✔
379
                                IdealFeeRate: s.IdealFeeRate,
5✔
380
                                ShutdownScripts: ShutdownScripts{
5✔
381
                                        LocalDeliveryScript:  s.LocalDeliveryScript, //nolint:ll
5✔
382
                                        RemoteDeliveryScript: msg.ShutdownScript,    //nolint:ll
5✔
383
                                },
5✔
384
                        },
5✔
385
                        NewEvents: newEvents,
5✔
386
                }, nil
5✔
387

388
        // If we get this message, then this means that we were finally able to
389
        // send out shutdown after receiving it from the remote party. We'll
390
        // now transition directly to the ChannelFlushing state.
391
        case *ShutdownComplete:
5✔
392
                chancloserLog.Infof("ChannelPoint(%v): waiting for channel to "+
5✔
393
                        "be flushed...", env.ChanPoint)
5✔
394

5✔
395
                // If the channel is *already* flushed, and the close is
5✔
396
                // already in progress, then we can skip the flushing state and
5✔
397
                // go straight into negotiation, as this is the RBF loop.
5✔
398
                var eventsToEmit []ProtocolEvent
5✔
399
                finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
5✔
400
                        unknownBalance,
5✔
401
                )
5✔
402
                if finalBalances != unknownBalance {
8✔
403
                        channelFlushed := ProtocolEvent(&ChannelFlushed{
3✔
404
                                ShutdownBalances: finalBalances,
3✔
405
                        })
3✔
406
                        eventsToEmit = append(eventsToEmit, channelFlushed)
3✔
407
                }
3✔
408

409
                // If we received a remote offer early from the remote party,
410
                // then we'll add that to the set of internal events to emit.
411
                s.EarlyRemoteOffer.WhenSome(func(offer OfferReceivedEvent) {
6✔
412
                        eventsToEmit = append(eventsToEmit, &offer)
1✔
413
                })
1✔
414

415
                var newEvents fn.Option[RbfEvent]
5✔
416
                if len(eventsToEmit) > 0 {
9✔
417
                        newEvents = fn.Some(RbfEvent{
4✔
418
                                InternalEvent: eventsToEmit,
4✔
419
                        })
4✔
420
                }
4✔
421

422
                // From here, we'll transition to the channel flushing state.
423
                // We'll stay here until we receive the ChannelFlushed event.
424
                return &CloseStateTransition{
5✔
425
                        NextState: &ChannelFlushing{
5✔
426
                                IdealFeeRate:    s.IdealFeeRate,
5✔
427
                                ShutdownScripts: s.ShutdownScripts,
5✔
428
                        },
5✔
429
                        NewEvents: newEvents,
5✔
430
                }, nil
5✔
431

432
        // Any other messages in this state will result in an error, as this is
433
        // an undefined state transition.
434
        default:
1✔
435
                return nil, fmt.Errorf("%w: received %T while in "+
1✔
436
                        "ShutdownPending", ErrInvalidStateTransition, msg)
1✔
437
        }
438
}
439

440
// ProcessEvent takes a new protocol event, and figures out if we can
441
// transition to the next state, or just loop back upon ourself. If we receive
442
// a ShutdownReceived event, then we'll stay in the ChannelFlushing state, as
443
// we haven't yet fully cleared the channel. Otherwise, we can move to the
444
// CloseReady state which'll being the channel closing process.
445
func (c *ChannelFlushing) ProcessEvent(event ProtocolEvent,
446
        env *Environment) (*CloseStateTransition, error) {
13✔
447

13✔
448
        switch msg := event.(type) {
13✔
449
        // If we get a confirmation, then a prior transaction we broadcasted
450
        // has confirmed, so we can move to our terminal state early.
451
        case *SpendEvent:
1✔
452
                return &CloseStateTransition{
1✔
453
                        NextState: &CloseFin{
1✔
454
                                ConfirmedTx: msg.Tx,
1✔
455
                        },
1✔
456
                }, nil
1✔
457

458
        // If we get an OfferReceived event, then the channel is flushed from
459
        // the PoV of the remote party. However, due to propagation delay or
460
        // concurrency, we may not have received the ChannelFlushed event yet.
461
        // In this case, we'll stash the event and wait for the ChannelFlushed
462
        // event.
463
        case *OfferReceivedEvent:
6✔
464
                chancloserLog.Infof("ChannelPoint(%v): received remote offer "+
6✔
465
                        "early, stashing...", env.ChanPoint)
6✔
466

6✔
467
                c.EarlyRemoteOffer = fn.Some(*msg)
6✔
468

6✔
469
                // We'll perform a noop update so we can wait for the actual
6✔
470
                // channel flushed event.
6✔
471
                return &CloseStateTransition{
6✔
472
                        NextState: c,
6✔
473
                }, nil
6✔
474

475
        // If we receive the ChannelFlushed event, then the coast is clear so
476
        // we'll now morph into the dual peer state so we can handle any
477
        // messages needed to drive forward the close process.
478
        case *ChannelFlushed:
8✔
479
                // Both the local and remote losing negotiation needs the terms
8✔
480
                // we'll be using to close the channel, so we'll create them
8✔
481
                // here.
8✔
482
                closeTerms := CloseChannelTerms{
8✔
483
                        ShutdownScripts:  c.ShutdownScripts,
8✔
484
                        ShutdownBalances: msg.ShutdownBalances,
8✔
485
                }
8✔
486

8✔
487
                chancloserLog.Infof("ChannelPoint(%v): channel flushed! "+
8✔
488
                        "proceeding with co-op close", env.ChanPoint)
8✔
489

8✔
490
                // Now that the channel has been flushed, we'll mark on disk
8✔
491
                // that we're approaching the point of no return where we'll
8✔
492
                // send a new signature to the remote party.
8✔
493
                //
8✔
494
                // TODO(roasbeef): doesn't actually matter if initiator here?
8✔
495
                if msg.FreshFlush {
13✔
496
                        err := env.ChanObserver.MarkCoopBroadcasted(nil, true)
5✔
497
                        if err != nil {
5✔
498
                                return nil, err
×
499
                        }
×
500
                }
501

502
                // If an ideal fee rate was specified, then we'll use that,
503
                // otherwise we'll fall back to the default value given in the
504
                // env.
505
                idealFeeRate := c.IdealFeeRate.UnwrapOr(env.DefaultFeeRate)
8✔
506

8✔
507
                // We'll then use that fee rate to determine the absolute fee
8✔
508
                // we'd propose.
8✔
509
                localTxOut, remoteTxOut := closeTerms.DeriveCloseTxOuts()
8✔
510
                absoluteFee := env.FeeEstimator.EstimateFee(
8✔
511
                        env.ChanType, localTxOut, remoteTxOut,
8✔
512
                        idealFeeRate.FeePerKWeight(),
8✔
513
                )
8✔
514

8✔
515
                chancloserLog.Infof("ChannelPoint(%v): using ideal_fee=%v, "+
8✔
516
                        "absolute_fee=%v", env.ChanPoint, idealFeeRate,
8✔
517
                        absoluteFee)
8✔
518

8✔
519
                var (
8✔
520
                        internalEvents []ProtocolEvent
8✔
521
                        newEvents      fn.Option[RbfEvent]
8✔
522
                )
8✔
523

8✔
524
                // If we received a remote offer early from the remote party,
8✔
525
                // then we'll add that to the set of internal events to emit.
8✔
526
                c.EarlyRemoteOffer.WhenSome(func(offer OfferReceivedEvent) {
12✔
527
                        internalEvents = append(internalEvents, &offer)
4✔
528
                })
4✔
529

530
                // Only if we have enough funds to pay for the fees do we need
531
                // to emit a localOfferSign event.
532
                //
533
                // TODO(roasbeef): also only proceed if was higher than fee in
534
                // last round?
535
                if closeTerms.LocalCanPayFees(absoluteFee) {
13✔
536
                        // Each time we go into this negotiation flow, we'll
5✔
537
                        // kick off our local state with a new close attempt.
5✔
538
                        // So we'll emit a internal event to drive forward that
5✔
539
                        // part of the state.
5✔
540
                        localOfferSign := ProtocolEvent(&SendOfferEvent{
5✔
541
                                TargetFeeRate: idealFeeRate,
5✔
542
                        })
5✔
543
                        internalEvents = append(internalEvents, localOfferSign)
5✔
544
                } else {
11✔
545
                        chancloserLog.Infof("ChannelPoint(%v): unable to pay "+
6✔
546
                                "fees with local balance, skipping "+
6✔
547
                                "closing_complete", env.ChanPoint)
6✔
548
                }
6✔
549

550
                if len(internalEvents) > 0 {
14✔
551
                        newEvents = fn.Some(RbfEvent{
6✔
552
                                InternalEvent: internalEvents,
6✔
553
                        })
6✔
554
                }
6✔
555

556
                return &CloseStateTransition{
8✔
557
                        NextState: &ClosingNegotiation{
8✔
558
                                PeerState: lntypes.Dual[AsymmetricPeerState]{
8✔
559
                                        Local: &LocalCloseStart{
8✔
560
                                                CloseChannelTerms: &closeTerms,
8✔
561
                                        },
8✔
562
                                        Remote: &RemoteCloseStart{
8✔
563
                                                CloseChannelTerms: &closeTerms,
8✔
564
                                        },
8✔
565
                                },
8✔
566
                                CloseChannelTerms: &closeTerms,
8✔
567
                        },
8✔
568
                        NewEvents: newEvents,
8✔
569
                }, nil
8✔
570

571
        default:
1✔
572
                return nil, fmt.Errorf("%w: received %T while in "+
1✔
573
                        "ChannelFlushing", ErrInvalidStateTransition, msg)
1✔
574
        }
575
}
576

577
// processNegotiateEvent is a helper function that processes a new event to
578
// local channel state once we're in the ClosingNegotiation state.
579
func processNegotiateEvent(c *ClosingNegotiation, event ProtocolEvent,
580
        env *Environment,
581
        chanPeer lntypes.ChannelParty) (*CloseStateTransition, error) {
30✔
582

30✔
583
        targetPeerState := c.PeerState.GetForParty(chanPeer)
30✔
584

30✔
585
        // Drive forward the remote state based on the next event.
30✔
586
        transition, err := targetPeerState.ProcessEvent(
30✔
587
                event, env,
30✔
588
        )
30✔
589
        if err != nil {
34✔
590
                return nil, err
4✔
591
        }
4✔
592

593
        nextPeerState, ok := transition.NextState.(AsymmetricPeerState) //nolint:ll
26✔
594
        if !ok {
26✔
595
                return nil, fmt.Errorf("expected %T to be "+
×
596
                        "AsymmetricPeerState", transition.NextState)
×
597
        }
×
598

599
        // Make a copy of the input state, then update the peer state of the
600
        // proper party.
601
        newPeerState := *c
26✔
602
        newPeerState.PeerState.SetForParty(chanPeer, nextPeerState)
26✔
603

26✔
604
        return &CloseStateTransition{
26✔
605
                NextState: &newPeerState,
26✔
606
                NewEvents: transition.NewEvents,
26✔
607
        }, nil
26✔
608
}
609

610
// updateAndValidateCloseTerms is a helper function that validates examines the
611
// incoming event, and decide if we need to update the remote party's address,
612
// or reject it if it doesn't include our latest address.
613
func (c *ClosingNegotiation) updateAndValidateCloseTerms(
614
        event ProtocolEvent) error {
34✔
615

34✔
616
        assertLocalScriptMatches := func(localScriptInMsg []byte) error {
54✔
617
                if !bytes.Equal(
20✔
618
                        c.LocalDeliveryScript, localScriptInMsg,
20✔
619
                ) {
22✔
620

2✔
621
                        return fmt.Errorf("%w: remote party sent wrong "+
2✔
622
                                "script, expected %x, got %x",
2✔
623
                                ErrWrongLocalScript, c.LocalDeliveryScript,
2✔
624
                                localScriptInMsg,
2✔
625
                        )
2✔
626
                }
2✔
627

628
                return nil
18✔
629
        }
630

631
        switch msg := event.(type) {
34✔
632
        // The remote party is sending us a new request to counter sign their
633
        // version of the commitment transaction.
634
        case *OfferReceivedEvent:
14✔
635
                // Make sure that they're sending our local script, and not
14✔
636
                // something else.
14✔
637
                err := assertLocalScriptMatches(msg.SigMsg.CloseeScript)
14✔
638
                if err != nil {
15✔
639
                        return err
1✔
640
                }
1✔
641

642
                oldRemoteAddr := c.RemoteDeliveryScript
13✔
643
                newRemoteAddr := msg.SigMsg.CloserScript
13✔
644

13✔
645
                // If they're sending a new script, then we'll update to the new
13✔
646
                // one.
13✔
647
                if !bytes.Equal(oldRemoteAddr, newRemoteAddr) {
14✔
648
                        c.RemoteDeliveryScript = newRemoteAddr
1✔
649
                }
1✔
650

651
        // The remote party responded to our sig request with a signature for
652
        // our version of the commitment transaction.
653
        case *LocalSigReceived:
9✔
654
                // Make sure that they're sending our local script, and not
9✔
655
                // something else.
9✔
656
                err := assertLocalScriptMatches(msg.SigMsg.CloserScript)
9✔
657
                if err != nil {
10✔
658
                        return err
1✔
659
                }
1✔
660

661
                return nil
8✔
662
        }
663

664
        return nil
27✔
665
}
666

667
// ProcessEvent drives forward the composite states for the local and remote
668
// party in response to new events. From this state, we'll continue to drive
669
// forward the local and remote states until we arrive at the StateFin stage,
670
// or we loop back up to the ShutdownPending state.
671
func (c *ClosingNegotiation) ProcessEvent(event ProtocolEvent,
672
        env *Environment) (*CloseStateTransition, error) {
37✔
673

37✔
674
        // There're two classes of events that can break us out of this state:
37✔
675
        // we receive a confirmation event, or we receive a signal to restart
37✔
676
        // the co-op close process.
37✔
677
        switch msg := event.(type) {
37✔
678
        // Ignore any potential duplicate channel flushed events.
679
        case *ChannelFlushed:
3✔
680
                return &CloseStateTransition{
3✔
681
                        NextState: c,
3✔
682
                }, nil
3✔
683

684
        // If we get a confirmation, then the spend request we issued when we
685
        // were leaving the ChannelFlushing state has been confirmed.  We'll
686
        // now transition to the StateFin state.
687
        case *SpendEvent:
6✔
688
                return &CloseStateTransition{
6✔
689
                        NextState: &CloseFin{
6✔
690
                                ConfirmedTx: msg.Tx,
6✔
691
                        },
6✔
692
                }, nil
6✔
693
        }
694

695
        // At this point, we know its a new signature message. We'll validate,
696
        // and maybe update the set of close terms based on what we receive. We
697
        // might update the remote party's address for example.
698
        if err := c.updateAndValidateCloseTerms(event); err != nil {
36✔
699
                return nil, fmt.Errorf("event violates close terms: %w", err)
2✔
700
        }
2✔
701

702
        shouldRouteTo := func(party lntypes.ChannelParty) bool {
76✔
703
                state := c.PeerState.GetForParty(party)
44✔
704
                if state == nil {
47✔
705
                        return false
3✔
706
                }
3✔
707

708
                return state.ShouldRouteTo(event)
41✔
709
        }
710

711
        // If we get to this point, then we have an event that'll drive forward
712
        // the negotiation process.  Based on the event, we'll figure out which
713
        // state we'll be modifying.
714
        switch {
32✔
715
        case shouldRouteTo(lntypes.Local):
20✔
716
                chancloserLog.Infof("ChannelPoint(%v): routing %T to local "+
20✔
717
                        "chan state", env.ChanPoint, event)
20✔
718

20✔
719
                // Drive forward the local state based on the next event.
20✔
720
                return processNegotiateEvent(c, event, env, lntypes.Local)
20✔
721

722
        case shouldRouteTo(lntypes.Remote):
13✔
723
                chancloserLog.Infof("ChannelPoint(%v): routing %T to remote "+
13✔
724
                        "chan state", env.ChanPoint, event)
13✔
725

13✔
726
                // Drive forward the remote state based on the next event.
13✔
727
                return processNegotiateEvent(c, event, env, lntypes.Remote)
13✔
728
        }
729

730
        return nil, fmt.Errorf("%w: received %T while in %v",
2✔
731
                ErrInvalidStateTransition, event, c)
2✔
732
}
733

734
// newSigTlv is a helper function that returns a new optional TLV sig field for
735
// the parametrized tlv.TlvType value.
736
func newSigTlv[T tlv.TlvType](s lnwire.Sig) tlv.OptionalRecordT[T, lnwire.Sig] {
24✔
737
        return tlv.SomeRecordT(tlv.NewRecordT[T](s))
24✔
738
}
24✔
739

740
// ProcessEvent implements the event processing to kick off the process of
741
// obtaining a new (possibly RBF'd) signature for our commitment transaction.
742
func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent,
743
        env *Environment) (*CloseStateTransition, error) {
13✔
744

13✔
745
        switch msg := event.(type) { //nolint:gocritic
13✔
746
        // If we receive a SendOfferEvent, then we'll use the specified fee
747
        // rate to generate for the closing transaction with our ideal fee
748
        // rate.
749
        case *SendOfferEvent:
13✔
750
                // given the state of the local/remote outputs.
13✔
751
                // First, we'll figure out the absolute fee rate we should pay
13✔
752
                localTxOut, remoteTxOut := l.DeriveCloseTxOuts()
13✔
753
                absoluteFee := env.FeeEstimator.EstimateFee(
13✔
754
                        env.ChanType, localTxOut, remoteTxOut,
13✔
755
                        msg.TargetFeeRate.FeePerKWeight(),
13✔
756
                )
13✔
757

13✔
758
                // If we can't actually pay for fees here, then we'll just do a
13✔
759
                // noop back to the same state to await a new fee rate.
13✔
760
                if !l.LocalCanPayFees(absoluteFee) {
17✔
761
                        chancloserLog.Infof("ChannelPoint(%v): unable to pay "+
4✔
762
                                "fee=%v with local balance %v, skipping "+
4✔
763
                                "closing_complete", env.ChanPoint, absoluteFee,
4✔
764
                                l.LocalBalance)
4✔
765

4✔
766
                        return &CloseStateTransition{
4✔
767
                                NextState: &CloseErr{
4✔
768
                                        CloseChannelTerms: l.CloseChannelTerms,
4✔
769
                                        Party:             lntypes.Local,
4✔
770
                                        ErrState: NewErrStateCantPayForFee(
4✔
771
                                                l.LocalBalance.ToSatoshis(),
4✔
772
                                                absoluteFee,
4✔
773
                                        ),
4✔
774
                                },
4✔
775
                        }, nil
4✔
776
                }
4✔
777

778
                // Now that we know what fee we want to pay, we'll create a new
779
                // signature over our co-op close transaction. For our
780
                // proposals, we'll just always use the known RBF sequence
781
                // value.
782
                localScript := l.LocalDeliveryScript
12✔
783
                rawSig, closeTx, closeBalance, err := env.CloseSigner.CreateCloseProposal( //nolint:ll
12✔
784
                        absoluteFee, localScript, l.RemoteDeliveryScript,
12✔
785
                        lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
12✔
786
                        lnwallet.WithCustomPayer(lntypes.Local),
12✔
787
                )
12✔
788
                if err != nil {
12✔
789
                        return nil, err
×
790
                }
×
791
                wireSig, err := lnwire.NewSigFromSignature(rawSig)
12✔
792
                if err != nil {
12✔
793
                        return nil, err
×
794
                }
×
795

796
                chancloserLog.Infof("closing w/ local_addr=%x, "+
12✔
797
                        "remote_addr=%x, fee=%v", localScript[:],
12✔
798
                        l.RemoteDeliveryScript[:], absoluteFee)
12✔
799

12✔
800
                chancloserLog.Infof("proposing closing_tx=%v",
12✔
801
                        lnutils.SpewLogClosure(closeTx))
12✔
802

12✔
803
                // Now that we have our signature, we'll set the proper
12✔
804
                // closingSigs field based on if the remote party's output is
12✔
805
                // dust or not.
12✔
806
                var closingSigs lnwire.ClosingSigs
12✔
807
                switch {
12✔
808
                // If the remote party's output is dust, then we'll set the
809
                // CloserNoClosee field.
810
                case remoteTxOut == nil:
4✔
811
                        closingSigs.CloserNoClosee = newSigTlv[tlv.TlvType1](
4✔
812
                                wireSig,
4✔
813
                        )
4✔
814

815
                // If after paying for fees, our balance is below dust, then
816
                // we'll set the NoCloserClosee field.
817
                case closeBalance < lnwallet.DustLimitForSize(len(localScript)):
1✔
818
                        closingSigs.NoCloserClosee = newSigTlv[tlv.TlvType2](
1✔
819
                                wireSig,
1✔
820
                        )
1✔
821

822
                // Otherwise, we'll set the CloserAndClosee field.
823
                //
824
                // TODO(roasbeef): should actually set both??
825
                default:
10✔
826
                        closingSigs.CloserAndClosee = newSigTlv[tlv.TlvType3](
10✔
827
                                wireSig,
10✔
828
                        )
10✔
829
                }
830

831
                // Now that we have our sig, we'll emit a daemon event to send
832
                // it to the remote party, then transition to the
833
                // LocalOfferSent state.
834
                //
835
                // TODO(roasbeef): type alias for protocol event
836
                sendEvent := protofsm.DaemonEventSet{&protofsm.SendMsgEvent[ProtocolEvent]{ //nolint:ll
12✔
837
                        TargetPeer: env.ChanPeer,
12✔
838
                        Msgs: []lnwire.Message{&lnwire.ClosingComplete{
12✔
839
                                ChannelID:    env.ChanID,
12✔
840
                                CloserScript: l.LocalDeliveryScript,
12✔
841
                                CloseeScript: l.RemoteDeliveryScript,
12✔
842
                                FeeSatoshis:  absoluteFee,
12✔
843
                                LockTime:     env.BlockHeight,
12✔
844
                                ClosingSigs:  closingSigs,
12✔
845
                        }},
12✔
846
                }}
12✔
847

12✔
848
                chancloserLog.Infof("ChannelPoint(%v): sending closing sig "+
12✔
849
                        "to remote party, fee_sats=%v", env.ChanPoint,
12✔
850
                        absoluteFee)
12✔
851

12✔
852
                return &CloseStateTransition{
12✔
853
                        NextState: &LocalOfferSent{
12✔
854
                                ProposedFee:       absoluteFee,
12✔
855
                                ProposedFeeRate:   msg.TargetFeeRate,
12✔
856
                                LocalSig:          wireSig,
12✔
857
                                CloseChannelTerms: l.CloseChannelTerms,
12✔
858
                        },
12✔
859
                        NewEvents: fn.Some(RbfEvent{
12✔
860
                                ExternalEvents: sendEvent,
12✔
861
                        }),
12✔
862
                }, nil
12✔
863
        }
864

865
        return nil, fmt.Errorf("%w: received %T while in LocalCloseStart",
×
866
                ErrInvalidStateTransition, event)
×
867
}
868

869
// extractSig extracts the expected signature from the closing sig message.
870
// Only one of them should actually be populated as the closing sig message is
871
// sent in response to a ClosingComplete message, it should only sign the same
872
// version of the co-op close tx as the sender did.
873
func extractSig(msg lnwire.ClosingSig) fn.Result[lnwire.Sig] {
8✔
874
        // First, we'll validate that only one signature is included in their
8✔
875
        // response to our initial offer. If not, then we'll exit here, and
8✔
876
        // trigger a recycle of the connection.
8✔
877
        sigInts := []bool{
8✔
878
                msg.CloserNoClosee.IsSome(), msg.NoCloserClosee.IsSome(),
8✔
879
                msg.CloserAndClosee.IsSome(),
8✔
880
        }
8✔
881
        numSigs := fn.Foldl(0, sigInts, func(acc int, sigInt bool) int {
26✔
882
                if sigInt {
27✔
883
                        return acc + 1
9✔
884
                }
9✔
885

886
                return acc
12✔
887
        })
888
        if numSigs != 1 {
9✔
889
                return fn.Errf[lnwire.Sig]("%w: only one sig should be set, "+
1✔
890
                        "got %v", ErrTooManySigs, numSigs)
1✔
891
        }
1✔
892

893
        // The final sig is the one that's actually set.
894
        sig := msg.CloserAndClosee.ValOpt().Alt(
7✔
895
                msg.NoCloserClosee.ValOpt(),
7✔
896
        ).Alt(
7✔
897
                msg.CloserNoClosee.ValOpt(),
7✔
898
        )
7✔
899

7✔
900
        return fn.NewResult(sig.UnwrapOrErr(ErrNoSig))
7✔
901
}
902

903
// ProcessEvent implements the state transition function for the
904
// LocalOfferSent state. In this state, we'll wait for the remote party to
905
// send a close_signed message which gives us the ability to broadcast a new
906
// co-op close transaction.
907
func (l *LocalOfferSent) ProcessEvent(event ProtocolEvent,
908
        env *Environment) (*CloseStateTransition, error) {
8✔
909

8✔
910
        switch msg := event.(type) { //nolint:gocritic
8✔
911
        // If we receive a LocalSigReceived event, then we'll attempt to
912
        // validate the signature from the remote party. If valid, then we can
913
        // broadcast the transaction, and transition to the ClosePending state.
914
        case *LocalSigReceived:
8✔
915
                // Extract and validate that only one sig field is set.
8✔
916
                sig, err := extractSig(msg.SigMsg).Unpack()
8✔
917
                if err != nil {
9✔
918
                        return nil, err
1✔
919
                }
1✔
920

921
                remoteSig, err := sig.ToSignature()
7✔
922
                if err != nil {
7✔
923
                        return nil, err
×
924
                }
×
925
                localSig, err := l.LocalSig.ToSignature()
7✔
926
                if err != nil {
7✔
927
                        return nil, err
×
928
                }
×
929

930
                // Now that we have their signature, we'll attempt to validate
931
                // it, then extract a valid closing signature from it.
932
                closeTx, _, err := env.CloseSigner.CompleteCooperativeClose(
7✔
933
                        localSig, remoteSig, l.LocalDeliveryScript,
7✔
934
                        l.RemoteDeliveryScript, l.ProposedFee,
7✔
935
                        lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
7✔
936
                        lnwallet.WithCustomPayer(lntypes.Local),
7✔
937
                )
7✔
938
                if err != nil {
7✔
939
                        return nil, err
×
940
                }
×
941

942
                // As we're about to broadcast a new version of the co-op close
943
                // transaction, we'll mark again as broadcast, but with this
944
                // variant of the co-op close tx.
945
                err = env.ChanObserver.MarkCoopBroadcasted(closeTx, true)
7✔
946
                if err != nil {
7✔
UNCOV
947
                        return nil, err
×
UNCOV
948
                }
×
949

950
                broadcastEvent := protofsm.DaemonEventSet{&protofsm.BroadcastTxn{ //nolint:ll
7✔
951
                        Tx: closeTx,
7✔
952
                        Label: labels.MakeLabel(
7✔
953
                                labels.LabelTypeChannelClose, &env.Scid,
7✔
954
                        ),
7✔
955
                }}
7✔
956

7✔
957
                chancloserLog.Infof("ChannelPoint(%v): received sig from "+
7✔
958
                        "remote party, broadcasting: tx=%v", env.ChanPoint,
7✔
959
                        lnutils.SpewLogClosure(closeTx),
7✔
960
                )
7✔
961

7✔
962
                return &CloseStateTransition{
7✔
963
                        NextState: &ClosePending{
7✔
964
                                CloseTx:           closeTx,
7✔
965
                                FeeRate:           l.ProposedFeeRate,
7✔
966
                                CloseChannelTerms: l.CloseChannelTerms,
7✔
967
                                Party:             lntypes.Local,
7✔
968
                        },
7✔
969
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
7✔
970
                                ExternalEvents: broadcastEvent,
7✔
971
                        }),
7✔
972
                }, nil
7✔
973
        }
974

975
        return nil, fmt.Errorf("%w: received %T while in LocalOfferSent",
×
976
                ErrInvalidStateTransition, event)
×
977
}
978

979
// ProcessEvent implements the state transition function for the
980
// RemoteCloseStart. In this state, we'll wait for the remote party to send a
981
// closing_complete message. Assuming they can pay for the fees, we'll sign it
982
// ourselves, then transition to the next state of ClosePending.
983
func (l *RemoteCloseStart) ProcessEvent(event ProtocolEvent,
984
        env *Environment) (*CloseStateTransition, error) {
11✔
985

11✔
986
        switch msg := event.(type) { //nolint:gocritic
11✔
987
        // If we receive a OfferReceived event, we'll make sure they can
988
        // actually pay for the fee. If so, then we'll counter sign and
989
        // transition to a terminal state.
990
        case *OfferReceivedEvent:
11✔
991
                // To start, we'll perform some basic validation of the sig
11✔
992
                // message they've sent. We'll validate that the remote party
11✔
993
                // actually has enough fees to pay the closing fees.
11✔
994
                if !l.RemoteCanPayFees(msg.SigMsg.FeeSatoshis) {
12✔
995
                        return nil, fmt.Errorf("%w: %v vs %v",
1✔
996
                                ErrRemoteCannotPay,
1✔
997
                                msg.SigMsg.FeeSatoshis,
1✔
998
                                l.RemoteBalance.ToSatoshis())
1✔
999
                }
1✔
1000

1001
                // With the basic sanity checks out of the way, we'll now
1002
                // figure out which signature that we'll attempt to sign
1003
                // against.
1004
                var (
10✔
1005
                        remoteSig input.Signature
10✔
1006
                        noClosee  bool
10✔
1007
                )
10✔
1008
                switch {
10✔
1009
                // If our balance is dust, then we expect the CloserNoClosee
1010
                // sig to be set.
1011
                case l.LocalAmtIsDust():
4✔
1012
                        if msg.SigMsg.CloserNoClosee.IsNone() {
5✔
1013
                                return nil, ErrCloserNoClosee
1✔
1014
                        }
1✔
1015
                        msg.SigMsg.CloserNoClosee.WhenSomeV(func(s lnwire.Sig) {
6✔
1016
                                remoteSig, _ = s.ToSignature()
3✔
1017
                                noClosee = true
3✔
1018
                        })
3✔
1019

1020
                // Otherwise, we'll assume that CloseAndClosee is set.
1021
                //
1022
                // TODO(roasbeef): NoCloserClosee, but makes no sense?
1023
                default:
9✔
1024
                        if msg.SigMsg.CloserAndClosee.IsNone() {
10✔
1025
                                return nil, ErrCloserAndClosee
1✔
1026
                        }
1✔
1027
                        msg.SigMsg.CloserAndClosee.WhenSomeV(func(s lnwire.Sig) { //nolint:ll
16✔
1028
                                remoteSig, _ = s.ToSignature()
8✔
1029
                        })
8✔
1030
                }
1031

1032
                chanOpts := []lnwallet.ChanCloseOpt{
8✔
1033
                        lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
8✔
1034
                        lnwallet.WithCustomLockTime(msg.SigMsg.LockTime),
8✔
1035
                        lnwallet.WithCustomPayer(lntypes.Remote),
8✔
1036
                }
8✔
1037

8✔
1038
                chancloserLog.Infof("responding to close w/ local_addr=%x, "+
8✔
1039
                        "remote_addr=%x, fee=%v",
8✔
1040
                        l.LocalDeliveryScript[:], l.RemoteDeliveryScript[:],
8✔
1041
                        msg.SigMsg.FeeSatoshis)
8✔
1042

8✔
1043
                // Now that we have the remote sig, we'll sign the version they
8✔
1044
                // signed, then attempt to complete the cooperative close
8✔
1045
                // process.
8✔
1046
                //
8✔
1047
                // TODO(roasbeef): need to be able to omit an output when
8✔
1048
                // signing based on the above, as closing opt
8✔
1049
                rawSig, _, _, err := env.CloseSigner.CreateCloseProposal(
8✔
1050
                        msg.SigMsg.FeeSatoshis, l.LocalDeliveryScript,
8✔
1051
                        l.RemoteDeliveryScript, chanOpts...,
8✔
1052
                )
8✔
1053
                if err != nil {
8✔
1054
                        return nil, err
×
1055
                }
×
1056
                wireSig, err := lnwire.NewSigFromSignature(rawSig)
8✔
1057
                if err != nil {
8✔
1058
                        return nil, err
×
1059
                }
×
1060

1061
                localSig, err := wireSig.ToSignature()
8✔
1062
                if err != nil {
8✔
1063
                        return nil, err
×
1064
                }
×
1065

1066
                // With our signature created, we'll now attempt to finalize the
1067
                // close process.
1068
                closeTx, _, err := env.CloseSigner.CompleteCooperativeClose(
8✔
1069
                        localSig, remoteSig, l.LocalDeliveryScript,
8✔
1070
                        l.RemoteDeliveryScript, msg.SigMsg.FeeSatoshis,
8✔
1071
                        chanOpts...,
8✔
1072
                )
8✔
1073
                if err != nil {
8✔
1074
                        return nil, err
×
1075
                }
×
1076

1077
                chancloserLog.Infof("ChannelPoint(%v): received sig (fee=%v "+
8✔
1078
                        "sats) from remote party, signing new tx=%v",
8✔
1079
                        env.ChanPoint, msg.SigMsg.FeeSatoshis,
8✔
1080
                        lnutils.SpewLogClosure(closeTx),
8✔
1081
                )
8✔
1082

8✔
1083
                var closingSigs lnwire.ClosingSigs
8✔
1084
                if noClosee {
11✔
1085
                        closingSigs.CloserNoClosee = newSigTlv[tlv.TlvType1](
3✔
1086
                                wireSig,
3✔
1087
                        )
3✔
1088
                } else {
11✔
1089
                        closingSigs.CloserAndClosee = newSigTlv[tlv.TlvType3](
8✔
1090
                                wireSig,
8✔
1091
                        )
8✔
1092
                }
8✔
1093

1094
                // As we're about to broadcast a new version of the co-op close
1095
                // transaction, we'll mark again as broadcast, but with this
1096
                // variant of the co-op close tx.
1097
                //
1098
                // TODO(roasbeef): db will only store one instance, store both?
1099
                err = env.ChanObserver.MarkCoopBroadcasted(closeTx, false)
8✔
1100
                if err != nil {
8✔
1101
                        return nil, err
×
1102
                }
×
1103

1104
                // As we transition, we'll omit two events: one to broadcast
1105
                // the transaction, and the other to send our ClosingSig
1106
                // message to the remote party.
1107
                sendEvent := &protofsm.SendMsgEvent[ProtocolEvent]{
8✔
1108
                        TargetPeer: env.ChanPeer,
8✔
1109
                        Msgs: []lnwire.Message{&lnwire.ClosingSig{
8✔
1110
                                ChannelID:    env.ChanID,
8✔
1111
                                CloserScript: l.RemoteDeliveryScript,
8✔
1112
                                CloseeScript: l.LocalDeliveryScript,
8✔
1113
                                FeeSatoshis:  msg.SigMsg.FeeSatoshis,
8✔
1114
                                LockTime:     msg.SigMsg.LockTime,
8✔
1115
                                ClosingSigs:  closingSigs,
8✔
1116
                        }},
8✔
1117
                }
8✔
1118
                broadcastEvent := &protofsm.BroadcastTxn{
8✔
1119
                        Tx: closeTx,
8✔
1120
                        Label: labels.MakeLabel(
8✔
1121
                                labels.LabelTypeChannelClose, &env.Scid,
8✔
1122
                        ),
8✔
1123
                }
8✔
1124
                daemonEvents := protofsm.DaemonEventSet{
8✔
1125
                        sendEvent, broadcastEvent,
8✔
1126
                }
8✔
1127

8✔
1128
                // We'll also compute the final fee rate that the remote party
8✔
1129
                // paid based off the absolute fee and the size of the closing
8✔
1130
                // transaction.
8✔
1131
                vSize := mempool.GetTxVirtualSize(btcutil.NewTx(closeTx))
8✔
1132
                feeRate := chainfee.SatPerVByte(
8✔
1133
                        int64(msg.SigMsg.FeeSatoshis) / vSize,
8✔
1134
                )
8✔
1135

8✔
1136
                // Now that we've extracted the signature, we'll transition to
8✔
1137
                // the next state where we'll sign+broadcast the sig.
8✔
1138
                return &CloseStateTransition{
8✔
1139
                        NextState: &ClosePending{
8✔
1140
                                CloseTx:           closeTx,
8✔
1141
                                FeeRate:           feeRate,
8✔
1142
                                CloseChannelTerms: l.CloseChannelTerms,
8✔
1143
                                Party:             lntypes.Remote,
8✔
1144
                        },
8✔
1145
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
8✔
1146
                                ExternalEvents: daemonEvents,
8✔
1147
                        }),
8✔
1148
                }, nil
8✔
1149
        }
1150

1151
        return nil, fmt.Errorf("%w: received %T while in RemoteCloseStart",
×
1152
                ErrInvalidStateTransition, event)
×
1153
}
1154

1155
// ProcessEvent is a semi-terminal state in the rbf-coop close state machine.
1156
// In this state, we're waiting for either a confirmation, or for either side
1157
// to attempt to create a new RBF'd co-op close transaction.
1158
func (c *ClosePending) ProcessEvent(event ProtocolEvent,
1159
        _ *Environment) (*CloseStateTransition, error) {
5✔
1160

5✔
1161
        switch msg := event.(type) {
5✔
1162
        // If we can a spend while waiting for the close, then we'll go to our
1163
        // terminal state.
1164
        case *SpendEvent:
×
1165
                return &CloseStateTransition{
×
1166
                        NextState: &CloseFin{
×
1167
                                ConfirmedTx: msg.Tx,
×
1168
                        },
×
1169
                }, nil
×
1170

1171
        // If we get a send offer event in this state, then we're doing a state
1172
        // transition to the LocalCloseStart state, so we can sign a new closing
1173
        // tx.
1174
        case *SendOfferEvent:
4✔
1175
                return &CloseStateTransition{
4✔
1176
                        NextState: &LocalCloseStart{
4✔
1177
                                CloseChannelTerms: c.CloseChannelTerms,
4✔
1178
                        },
4✔
1179
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
4✔
1180
                                InternalEvent: []ProtocolEvent{msg},
4✔
1181
                        }),
4✔
1182
                }, nil
4✔
1183

1184
        // If we get an offer received event, then we're doing a state
1185
        // transition to the RemoteCloseStart, as the remote peer wants to sign
1186
        // a new closing tx.
1187
        case *OfferReceivedEvent:
4✔
1188
                return &CloseStateTransition{
4✔
1189
                        NextState: &RemoteCloseStart{
4✔
1190
                                CloseChannelTerms: c.CloseChannelTerms,
4✔
1191
                        },
4✔
1192
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
4✔
1193
                                InternalEvent: []ProtocolEvent{msg},
4✔
1194
                        }),
4✔
1195
                }, nil
4✔
1196

1197
        default:
×
1198

×
1199
                return &CloseStateTransition{
×
1200
                        NextState: c,
×
1201
                }, nil
×
1202
        }
1203
}
1204

1205
// ProcessEvent is the event processing for out terminal state. In this state,
1206
// we just keep looping back on ourselves.
1207
func (c *CloseFin) ProcessEvent(_ ProtocolEvent,
1208
        _ *Environment) (*CloseStateTransition, error) {
×
1209

×
1210
        return &CloseStateTransition{
×
1211
                NextState: c,
×
1212
        }, nil
×
1213
}
×
1214

1215
// ProcessEvent is a semi-terminal state in the rbf-coop close state machine.
1216
// In this state, we hit a validation error in an earlier state, so we'll remain
1217
// in this state for the user to examine. We may also process new requests to
1218
// continue the state machine.
1219
func (c *CloseErr) ProcessEvent(event ProtocolEvent,
1220
        _ *Environment) (*CloseStateTransition, error) {
2✔
1221

2✔
1222
        switch msg := event.(type) {
2✔
1223
        // If we get a send offer event in this state, then we're doing a state
1224
        // transition to the LocalCloseStart state, so we can sign a new closing
1225
        // tx.
1226
        case *SendOfferEvent:
1✔
1227
                return &CloseStateTransition{
1✔
1228
                        NextState: &LocalCloseStart{
1✔
1229
                                CloseChannelTerms: c.CloseChannelTerms,
1✔
1230
                        },
1✔
1231
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
1✔
1232
                                InternalEvent: []ProtocolEvent{msg},
1✔
1233
                        }),
1✔
1234
                }, nil
1✔
1235

1236
        // If we get an offer received event, then we're doing a state
1237
        // transition to the RemoteCloseStart, as the remote peer wants to sign
1238
        // a new closing tx.
1239
        case *OfferReceivedEvent:
1✔
1240
                return &CloseStateTransition{
1✔
1241
                        NextState: &RemoteCloseStart{
1✔
1242
                                CloseChannelTerms: c.CloseChannelTerms,
1✔
1243
                        },
1✔
1244
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
1✔
1245
                                InternalEvent: []ProtocolEvent{msg},
1✔
1246
                        }),
1✔
1247
                }, nil
1✔
1248

1249
        default:
×
1250
                return &CloseStateTransition{
×
1251
                        NextState: c,
×
1252
                }, nil
×
1253
        }
1254
}
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