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

lightningnetwork / lnd / 13558005087

27 Feb 2025 03:04AM UTC coverage: 58.834% (-0.001%) from 58.835%
13558005087

Pull #8453

github

Roasbeef
lnwallet/chancloser: increase test coverage of state machine
Pull Request #8453: [4/4] - multi: integrate new rbf coop close FSM into the existing peer flow

1079 of 1370 new or added lines in 23 files covered. (78.76%)

578 existing lines in 40 files now uncovered.

137063 of 232965 relevant lines covered (58.83%)

19205.84 hits per line

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

91.65
/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/davecgh/go-spew/spew"
13
        "github.com/lightningnetwork/lnd/fn/v2"
14
        "github.com/lightningnetwork/lnd/input"
15
        "github.com/lightningnetwork/lnd/labels"
16
        "github.com/lightningnetwork/lnd/lntypes"
17
        "github.com/lightningnetwork/lnd/lnutils"
18
        "github.com/lightningnetwork/lnd/lnwallet"
19
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
20
        "github.com/lightningnetwork/lnd/lnwire"
21
        "github.com/lightningnetwork/lnd/protofsm"
22
        "github.com/lightningnetwork/lnd/tlv"
23
)
24

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

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

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

56
                        return ok
3✔
57
                }),
58
                PostSendEvent: postSendEvent,
59
        }
60

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

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

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

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

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

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

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

3✔
98
        return protofsm.DaemonEventSet{msgsToSend}, nil
3✔
99
}
100

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

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

123
                return nil
1✔
124
        })
125
        if err != nil {
7✔
126
                return err
1✔
127
        }
1✔
128

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

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

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

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

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

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

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

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

3✔
207
                // Validate that they can send the message now, and also that
3✔
208
                // they haven't violated their commitment to a prior upfront
3✔
209
                // shutdown addr.
3✔
210
                err := validateShutdown(
3✔
211
                        env.ThawHeight, env.RemoteUpfrontShutdown, msg,
3✔
212
                        env.ChanPoint, env.ChainParams,
3✔
213
                )
3✔
214
                if err != nil {
4✔
215
                        chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
1✔
216
                                "shutdown attempt: %v", 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(
2✔
224
                        env.NewDeliveryScript,
2✔
225
                )
2✔
226
                if err != nil {
2✔
227
                        return nil, err
×
228
                }
×
229

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

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

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

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

257
                remoteAddr := msg.ShutdownScript
2✔
258

2✔
259
                return &CloseStateTransition{
2✔
260
                        NextState: &ShutdownPending{
2✔
261
                                ShutdownScripts: ShutdownScripts{
2✔
262
                                        LocalDeliveryScript:  shutdownAddr,
2✔
263
                                        RemoteDeliveryScript: remoteAddr,
2✔
264
                                },
2✔
265
                        },
2✔
266
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
2✔
267
                                ExternalEvents: daemonEvents,
2✔
268
                        }),
2✔
269
                }, nil
2✔
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, env *Environment,
287
) (*CloseStateTransition, error) {
11✔
288

11✔
289
        switch msg := event.(type) {
11✔
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:
4✔
318
                chancloserLog.Infof("ChannelPoint(%v): received shutdown msg",
4✔
319
                        env.ChanPoint)
4✔
320

4✔
321
                // Validate that they can send the message now, and also that
4✔
322
                // they haven't violated their commitment to a prior upfront
4✔
323
                // shutdown addr.
4✔
324
                err := validateShutdown(
4✔
325
                        env.ThawHeight, env.RemoteUpfrontShutdown, msg,
4✔
326
                        env.ChanPoint, env.ChainParams,
4✔
327
                )
4✔
328
                if err != nil {
5✔
329
                        chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
1✔
330
                                "shutdown attempt: %v", 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
3✔
339
                finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
3✔
340
                        unknownBalance,
3✔
341
                )
3✔
342
                if finalBalances != unknownBalance {
4✔
343
                        channelFlushed := ProtocolEvent(&ChannelFlushed{
1✔
344
                                ShutdownBalances: finalBalances,
1✔
345
                        })
1✔
346
                        eventsToEmit = append(eventsToEmit, channelFlushed)
1✔
347
                }
1✔
348

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

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

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

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

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

375
                // We transition to the ChannelFlushing state, where we await
376
                // the ChannelFlushed event.
377
                return &CloseStateTransition{
3✔
378
                        NextState: &ChannelFlushing{
3✔
379
                                IdealFeeRate: s.IdealFeeRate,
3✔
380
                                ShutdownScripts: ShutdownScripts{
3✔
381
                                        LocalDeliveryScript:  s.LocalDeliveryScript, //nolint:ll
3✔
382
                                        RemoteDeliveryScript: msg.ShutdownScript,    //nolint:ll
3✔
383
                                },
3✔
384
                        },
3✔
385
                        NewEvents: newEvents,
3✔
386
                }, nil
3✔
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:
3✔
392
                chancloserLog.Infof("ChannelPoint(%v): waiting for channel to "+
3✔
393
                        "be flushed...", env.ChanPoint)
3✔
394

3✔
395
                // If the channel is *already* flushed, and the close is
3✔
396
                // already in progress, then we can skip the flushing state and
3✔
397
                // go straight into negotiation, as this is the RBF loop.
3✔
398
                var eventsToEmit []ProtocolEvent
3✔
399
                finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
3✔
400
                        unknownBalance,
3✔
401
                )
3✔
402
                if finalBalances != unknownBalance {
4✔
403
                        channelFlushed := ProtocolEvent(&ChannelFlushed{
1✔
404
                                ShutdownBalances: finalBalances,
1✔
405
                        })
1✔
406
                        eventsToEmit = append(eventsToEmit, channelFlushed)
1✔
407
                }
1✔
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) {
4✔
412
                        eventsToEmit = append(eventsToEmit, &offer)
1✔
413
                })
1✔
414

415
                var newEvents fn.Option[RbfEvent]
3✔
416
                if len(eventsToEmit) > 0 {
5✔
417
                        newEvents = fn.Some(RbfEvent{
2✔
418
                                InternalEvent: eventsToEmit,
2✔
419
                        })
2✔
420
                }
2✔
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{
3✔
425
                        NextState: &ChannelFlushing{
3✔
426
                                IdealFeeRate:    s.IdealFeeRate,
3✔
427
                                ShutdownScripts: s.ShutdownScripts,
3✔
428
                        },
3✔
429
                        NewEvents: newEvents,
3✔
430
                }, nil
3✔
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, env *Environment,
446
) (*CloseStateTransition, error) {
9✔
447

9✔
448
        switch msg := event.(type) {
9✔
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:
3✔
464
                chancloserLog.Infof("ChannelPoint(%v): received remote offer "+
3✔
465
                        "early, stashing...", env.ChanPoint)
3✔
466

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

3✔
469
                // We'll perform a noop update so we can wait for the actual
3✔
470
                // channel flushed event.
3✔
471
                return &CloseStateTransition{
3✔
472
                        NextState: c,
3✔
473
                }, nil
3✔
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:
5✔
479
                // Both the local and remote losing negotiation needs the terms
5✔
480
                // we'll be using to close the channel, so we'll create them
5✔
481
                // here.
5✔
482
                closeTerms := CloseChannelTerms{
5✔
483
                        ShutdownScripts:  c.ShutdownScripts,
5✔
484
                        ShutdownBalances: msg.ShutdownBalances,
5✔
485
                }
5✔
486

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

5✔
490
                // Now that the channel has been flushed, we'll mark on disk
5✔
491
                // that we're approaching the point of no return where we'll
5✔
492
                // send a new signature to the remote party.
5✔
493
                //
5✔
494
                // TODO(roasbeef): doesn't actually matter if initiator here?
5✔
495
                if msg.FreshFlush {
8✔
496
                        err := env.ChanObserver.MarkCoopBroadcasted(nil, true)
3✔
497
                        if err != nil {
3✔
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)
5✔
506

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

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

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

5✔
524
                // If we received a remote offer early from the remote party,
5✔
525
                // then we'll add that to the set of internal events to emit.
5✔
526
                c.EarlyRemoteOffer.WhenSome(func(offer OfferReceivedEvent) {
6✔
527
                        internalEvents = append(internalEvents, &offer)
1✔
528
                })
1✔
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) {
8✔
536
                        // Each time we go into this negotiation flow, we'll
3✔
537
                        // kick off our local state with a new close attempt.
3✔
538
                        // So we'll emit a internal event to drive forward that
3✔
539
                        // part of the state.
3✔
540
                        localOfferSign := ProtocolEvent(&SendOfferEvent{
3✔
541
                                TargetFeeRate: idealFeeRate,
3✔
542
                        })
3✔
543
                        internalEvents = append(internalEvents, localOfferSign)
3✔
544
                } else {
6✔
545
                        chancloserLog.Infof("ChannelPoint(%v): unable to pay "+
3✔
546
                                "fees with local balance, skipping "+
3✔
547
                                "closing_complete", env.ChanPoint)
3✔
548
                }
3✔
549

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

556
                return &CloseStateTransition{
5✔
557
                        NextState: &ClosingNegotiation{
5✔
558
                                PeerState: lntypes.Dual[AsymmetricPeerState]{
5✔
559
                                        Local: &LocalCloseStart{
5✔
560
                                                CloseChannelTerms: &closeTerms,
5✔
561
                                        },
5✔
562
                                        Remote: &RemoteCloseStart{
5✔
563
                                                CloseChannelTerms: &closeTerms,
5✔
564
                                        },
5✔
565
                                },
5✔
566
                                CloseChannelTerms: &closeTerms,
5✔
567
                        },
5✔
568
                        NewEvents: newEvents,
5✔
569
                }, nil
5✔
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, chanPeer lntypes.ChannelParty,
581
) (*CloseStateTransition, error) {
27✔
582

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

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

593
        nextPeerState, ok := transition.NextState.(AsymmetricPeerState) //nolint:ll
23✔
594
        if !ok {
23✔
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
23✔
602
        newPeerState.PeerState.SetForParty(chanPeer, nextPeerState)
23✔
603

23✔
604
        return &CloseStateTransition{
23✔
605
                NextState: &newPeerState,
23✔
606
                NewEvents: transition.NewEvents,
23✔
607
        }, nil
23✔
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(event ProtocolEvent,
614
) error {
31✔
615

31✔
616
        assertLocalScriptMatches := func(localScriptInMsg []byte) error {
48✔
617
                if !bytes.Equal(
17✔
618
                        c.LocalDeliveryScript, localScriptInMsg,
17✔
619
                ) {
19✔
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
15✔
629
        }
630

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

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

10✔
645
                // If they're sending a new script, then we'll update to the new
10✔
646
                // one.
10✔
647
                if !bytes.Equal(oldRemoteAddr, newRemoteAddr) {
11✔
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:
7✔
654
                // Make sure that they're sending our local script, and not
7✔
655
                // something else.
7✔
656
                err := assertLocalScriptMatches(msg.SigMsg.CloserScript)
7✔
657
                if err != nil {
8✔
658
                        return err
1✔
659
                }
1✔
660

661
                return nil
6✔
662
        }
663

664
        return nil
24✔
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, env *Environment,
672
) (*CloseStateTransition, error) {
34✔
673

34✔
674
        // There're two classes of events that can break us out of this state:
34✔
675
        // we receive a confirmation event, or we receive a signal to restart
34✔
676
        // the co-op close process.
34✔
677
        switch msg := event.(type) {
34✔
678
        // Ignore any potential duplicate channel flushed events.
679
        case *ChannelFlushed:
1✔
680
                return &CloseStateTransition{
1✔
681
                        NextState: c,
1✔
682
                }, nil
1✔
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:
4✔
688
                return &CloseStateTransition{
4✔
689
                        NextState: &CloseFin{
4✔
690
                                ConfirmedTx: msg.Tx,
4✔
691
                        },
4✔
692
                }, nil
4✔
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 {
33✔
699
                return nil, fmt.Errorf("event violates close terms: %w", err)
2✔
700
        }
2✔
701

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

708
                return state.ShouldRouteTo(event)
37✔
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 {
29✔
715
        case shouldRouteTo(lntypes.Local):
18✔
716
                chancloserLog.Infof("ChannelPoint(%v): routing %T to local "+
18✔
717
                        "chan state", env.ChanPoint, event)
18✔
718

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

722
        case shouldRouteTo(lntypes.Remote):
10✔
723
                chancloserLog.Infof("ChannelPoint(%v): routing %T to remote "+
10✔
724

10✔
725
                        "chan state", env.ChanPoint, event)
10✔
726
                // Drive forward the remote state based on the next event.
10✔
727
                return processNegotiateEvent(c, event, env, lntypes.Remote)
10✔
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] {
27✔
737
        return tlv.SomeRecordT(tlv.NewRecordT[T](s))
27✔
738
}
27✔
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, env *Environment,
743
) (*CloseStateTransition, error) {
11✔
744

11✔
745
        switch msg := event.(type) { //nolint:gocritic
11✔
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:
11✔
750
                // given the state of the local/remote outputs.
11✔
751
                // First, we'll figure out the absolute fee rate we should pay
11✔
752
                localTxOut, remoteTxOut := l.DeriveCloseTxOuts()
11✔
753
                absoluteFee := env.FeeEstimator.EstimateFee(
11✔
754
                        env.ChanType, localTxOut, remoteTxOut,
11✔
755
                        msg.TargetFeeRate.FeePerKWeight(),
11✔
756
                )
11✔
757

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

2✔
766
                        return &CloseStateTransition{
2✔
767
                                NextState: &CloseErr{
2✔
768
                                        CloseChannelTerms: l.CloseChannelTerms,
2✔
769
                                        Party:             lntypes.Local,
2✔
770
                                        ErrState: NewErrStateCantPayForFee(
2✔
771
                                                l.LocalBalance.ToSatoshis(),
2✔
772
                                                absoluteFee,
2✔
773
                                        ),
2✔
774
                                },
2✔
775
                        }, nil
2✔
776
                }
2✔
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
10✔
783
                rawSig, closeTx, closeBalance, err := env.CloseSigner.CreateCloseProposal( //nolint:ll
10✔
784
                        absoluteFee, localScript, l.RemoteDeliveryScript,
10✔
785
                        lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
10✔
786
                        lnwallet.WithCustomPayer(lntypes.Local),
10✔
787
                )
10✔
788
                if err != nil {
10✔
789
                        return nil, err
×
790
                }
×
791
                wireSig, err := lnwire.NewSigFromSignature(rawSig)
10✔
792
                if err != nil {
10✔
793
                        return nil, err
×
794
                }
×
795

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

10✔
800
                chancloserLog.Infof("proposing closing_tx=%v",
10✔
801
                        spew.Sdump(closeTx))
10✔
802

10✔
803
                // Now that we have our signature, we'll set the proper
10✔
804
                // closingSigs field based on if the remote party's output is
10✔
805
                // dust or not.
10✔
806
                var closingSigs lnwire.ClosingSigs
10✔
807
                switch {
10✔
808
                // If the remote party's output is dust, then we'll set the
809
                // CloserNoClosee field.
810
                case remoteTxOut == nil:
2✔
811
                        closingSigs.CloserNoClosee = newSigTlv[tlv.TlvType1](
2✔
812
                                wireSig,
2✔
813
                        )
2✔
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:
8✔
826
                        closingSigs.CloserAndClosee = newSigTlv[tlv.TlvType3](
8✔
827
                                wireSig,
8✔
828
                        )
8✔
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
10✔
837
                        TargetPeer: env.ChanPeer,
10✔
838
                        Msgs: []lnwire.Message{&lnwire.ClosingComplete{
10✔
839
                                ChannelID:    env.ChanID,
10✔
840
                                CloserScript: l.LocalDeliveryScript,
10✔
841
                                CloseeScript: l.RemoteDeliveryScript,
10✔
842
                                FeeSatoshis:  absoluteFee,
10✔
843
                                LockTime:     env.BlockHeight,
10✔
844
                                ClosingSigs:  closingSigs,
10✔
845
                        }},
10✔
846
                }}
10✔
847

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

10✔
852
                return &CloseStateTransition{
10✔
853
                        NextState: &LocalOfferSent{
10✔
854
                                ProposedFee:       absoluteFee,
10✔
855
                                ProposedFeeRate:   msg.TargetFeeRate,
10✔
856
                                LocalSig:          wireSig,
10✔
857
                                CloseChannelTerms: l.CloseChannelTerms,
10✔
858
                        },
10✔
859
                        NewEvents: fn.Some(RbfEvent{
10✔
860
                                ExternalEvents: sendEvent,
10✔
861
                        }),
10✔
862
                }, nil
10✔
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] {
6✔
874
        // First, we'll validate that only one signature is included in their
6✔
875
        // response to our initial offer. If not, then we'll exit here, and
6✔
876
        // trigger a recycle of the connection.
6✔
877
        sigInts := []bool{
6✔
878
                msg.CloserNoClosee.IsSome(), msg.NoCloserClosee.IsSome(),
6✔
879
                msg.CloserAndClosee.IsSome(),
6✔
880
        }
6✔
881
        numSigs := fn.Foldl(0, sigInts, func(acc int, sigInt bool) int {
22✔
882
                if sigInt {
23✔
883
                        return acc + 1
7✔
884
                }
7✔
885

886
                return acc
10✔
887
        })
888
        if numSigs != 1 {
7✔
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(
5✔
895
                msg.NoCloserClosee.ValOpt(),
5✔
896
        ).Alt(
5✔
897
                msg.CloserNoClosee.ValOpt(),
5✔
898
        )
5✔
899

5✔
900
        return fn.NewResult(sig.UnwrapOrErr(ErrNoSig))
5✔
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, env *Environment,
908
) (*CloseStateTransition, error) {
6✔
909

6✔
910
        switch msg := event.(type) { //nolint:gocritic
6✔
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:
6✔
915
                // Extract and validate that only one sig field is set.
6✔
916
                sig, err := extractSig(msg.SigMsg).Unpack()
6✔
917
                if err != nil {
7✔
918
                        return nil, err
1✔
919
                }
1✔
920

921
                remoteSig, err := sig.ToSignature()
5✔
922
                if err != nil {
5✔
923
                        return nil, err
×
924
                }
×
925
                localSig, err := l.LocalSig.ToSignature()
5✔
926
                if err != nil {
5✔
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(
5✔
933
                        localSig, remoteSig, l.LocalDeliveryScript,
5✔
934
                        l.RemoteDeliveryScript, l.ProposedFee,
5✔
935
                        lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
5✔
936
                        lnwallet.WithCustomPayer(lntypes.Local),
5✔
937
                )
5✔
938
                if err != nil {
5✔
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)
5✔
946
                if err != nil {
5✔
947
                        return nil, err
×
948
                }
×
949

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

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

5✔
962
                return &CloseStateTransition{
5✔
963
                        NextState: &ClosePending{
5✔
964
                                CloseTx:           closeTx,
5✔
965
                                FeeRate:           l.ProposedFeeRate,
5✔
966
                                CloseChannelTerms: l.CloseChannelTerms,
5✔
967
                                Party:             lntypes.Local,
5✔
968
                        },
5✔
969
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
5✔
970
                                ExternalEvents: broadcastEvent,
5✔
971
                        }),
5✔
972
                }, nil
5✔
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, env *Environment,
984
) (*CloseStateTransition, error) {
8✔
985

8✔
986
        switch msg := event.(type) { //nolint:gocritic
8✔
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:
8✔
991
                // To start, we'll perform some basic validation of the sig
8✔
992
                // message they've sent. We'll validate that the remote party
8✔
993
                // actually has enough fees to pay the closing fees.
8✔
994
                if !l.RemoteCanPayFees(msg.SigMsg.FeeSatoshis) {
9✔
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 (
7✔
1005
                        remoteSig input.Signature
7✔
1006
                        noClosee  bool
7✔
1007
                )
7✔
1008
                switch {
7✔
1009
                // If our balance is dust, then we expect the CloserNoClosee
1010
                // sig to be set.
1011
                case l.LocalAmtIsDust():
2✔
1012
                        if msg.SigMsg.CloserNoClosee.IsNone() {
3✔
1013
                                return nil, ErrCloserNoClosee
1✔
1014
                        }
1✔
1015
                        msg.SigMsg.CloserNoClosee.WhenSomeV(func(s lnwire.Sig) {
2✔
1016
                                remoteSig, _ = s.ToSignature()
1✔
1017
                                noClosee = true
1✔
1018
                        })
1✔
1019

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

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

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

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

1061
                localSig, err := wireSig.ToSignature()
5✔
1062
                if err != nil {
5✔
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(
5✔
1069
                        localSig, remoteSig, l.LocalDeliveryScript,
5✔
1070
                        l.RemoteDeliveryScript, msg.SigMsg.FeeSatoshis,
5✔
1071
                        chanOpts...,
5✔
1072
                )
5✔
1073
                if err != nil {
5✔
1074
                        return nil, err
×
1075
                }
×
1076

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

5✔
1083
                var closingSigs lnwire.ClosingSigs
5✔
1084
                if noClosee {
6✔
1085
                        closingSigs.CloserNoClosee = newSigTlv[tlv.TlvType1](
1✔
1086
                                wireSig,
1✔
1087
                        )
1✔
1088
                } else {
6✔
1089
                        closingSigs.CloserAndClosee = newSigTlv[tlv.TlvType3](
5✔
1090
                                wireSig,
5✔
1091
                        )
5✔
1092
                }
5✔
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)
5✔
1100
                if err != nil {
5✔
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]{
5✔
1108
                        TargetPeer: env.ChanPeer,
5✔
1109
                        Msgs: []lnwire.Message{&lnwire.ClosingSig{
5✔
1110
                                ChannelID:    env.ChanID,
5✔
1111
                                CloserScript: l.RemoteDeliveryScript,
5✔
1112
                                CloseeScript: l.LocalDeliveryScript,
5✔
1113
                                FeeSatoshis:  msg.SigMsg.FeeSatoshis,
5✔
1114
                                LockTime:     msg.SigMsg.LockTime,
5✔
1115
                                ClosingSigs:  closingSigs,
5✔
1116
                        }},
5✔
1117
                }
5✔
1118
                broadcastEvent := &protofsm.BroadcastTxn{
5✔
1119
                        Tx: closeTx,
5✔
1120
                        Label: labels.MakeLabel(
5✔
1121
                                labels.LabelTypeChannelClose, &env.Scid,
5✔
1122
                        ),
5✔
1123
                }
5✔
1124
                daemonEvents := protofsm.DaemonEventSet{
5✔
1125
                        sendEvent, broadcastEvent,
5✔
1126
                }
5✔
1127

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

5✔
1136
                // Now that we've extracted the signature, we'll transition to
5✔
1137
                // the next state where we'll sign+broadcast the sig.
5✔
1138
                return &CloseStateTransition{
5✔
1139
                        NextState: &ClosePending{
5✔
1140
                                CloseTx:           closeTx,
5✔
1141
                                FeeRate:           feeRate,
5✔
1142
                                CloseChannelTerms: l.CloseChannelTerms,
5✔
1143
                                Party:             lntypes.Remote,
5✔
1144
                        },
5✔
1145
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
5✔
1146
                                ExternalEvents: daemonEvents,
5✔
1147
                        }),
5✔
1148
                }, nil
5✔
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, env *Environment,
1159
) (*CloseStateTransition, error) {
3✔
1160

3✔
1161
        switch msg := event.(type) {
3✔
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:
2✔
1175
                return &CloseStateTransition{
2✔
1176
                        NextState: &LocalCloseStart{
2✔
1177
                                CloseChannelTerms: c.CloseChannelTerms,
2✔
1178
                        },
2✔
1179
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
2✔
1180
                                InternalEvent: []ProtocolEvent{msg},
2✔
1181
                        }),
2✔
1182
                }, nil
2✔
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:
2✔
1188
                return &CloseStateTransition{
2✔
1189
                        NextState: &RemoteCloseStart{
2✔
1190
                                CloseChannelTerms: c.CloseChannelTerms,
2✔
1191
                        },
2✔
1192
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
2✔
1193
                                InternalEvent: []ProtocolEvent{msg},
2✔
1194
                        }),
2✔
1195
                }, nil
2✔
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(event ProtocolEvent, env *Environment,
1208
) (*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, env *Environment,
1220
) (*CloseStateTransition, error) {
2✔
1221

2✔
1222
        switch msg := event.(type) {
2✔
1223

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

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

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