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

lightningnetwork / lnd / 15736109134

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

Pull #9752

github

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

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

28331 existing lines in 455 files now uncovered.

97860 of 168153 relevant lines covered (58.2%)

1.81 hits per line

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

77.83
/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 {
6✔
51
                                chancloserLog.Infof("ChannelPoint(%v): no "+
3✔
52
                                        "dangling updates sending shutdown "+
3✔
53
                                        "message", chanPoint)
3✔
54
                        }
3✔
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() {
6✔
64
                return protofsm.DaemonEventSet{msgsToSend}, nil
3✔
65
        }
3✔
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 {
3✔
108

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

123
                return nil
3✔
124
        })
125
        if err != nil {
3✔
UNCOV
126
                return err
×
UNCOV
127
        }
×
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 {
3✔
UNCOV
132
                return validateShutdownScript(
×
UNCOV
133
                        addr, msg.ShutdownScript, &chainParams,
×
UNCOV
134
                )
×
UNCOV
135
        })(upfrontAddr).UnwrapOr(nil)
×
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) {
3✔
144

3✔
145
        switch msg := event.(type) {
3✔
146
        // If we get a confirmation, then a prior transaction we broadcasted
147
        // has confirmed, so we can move to our terminal state early.
UNCOV
148
        case *SpendEvent:
×
UNCOV
149
                return &CloseStateTransition{
×
UNCOV
150
                        NextState: &CloseFin{
×
UNCOV
151
                                ConfirmedTx: msg.Tx,
×
UNCOV
152
                        },
×
UNCOV
153
                }, nil
×
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 {
3✔
UNCOV
166
                        return nil, err
×
UNCOV
167
                }
×
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(
3✔
174
                        env.ChanID, env.ChanPoint, shutdownScript,
3✔
175
                        env.ChanPeer, fn.None[ProtocolEvent](),
3✔
176
                        env.ChanObserver,
3✔
177
                )
3✔
178
                if err != nil {
3✔
179
                        return nil, err
×
180
                }
×
181

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

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

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

×
UNCOV
219
                        return nil, err
×
UNCOV
220
                }
×
221

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

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

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

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

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

258
                remoteAddr := msg.ShutdownScript
3✔
259

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

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

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

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

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

×
UNCOV
308
                s.EarlyRemoteOffer = fn.Some(*msg)
×
UNCOV
309

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

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

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

×
UNCOV
333
                        return nil, err
×
UNCOV
334
                }
×
335

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3✔
584
        targetPeerState := c.PeerState.GetForParty(chanPeer)
3✔
585

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

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

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

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

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

3✔
617
        assertLocalScriptMatches := func(localScriptInMsg []byte) error {
6✔
618
                if !bytes.Equal(
3✔
619
                        c.LocalDeliveryScript, localScriptInMsg,
3✔
620
                ) {
3✔
UNCOV
621

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

629
                return nil
3✔
630
        }
631

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

643
                oldRemoteAddr := c.RemoteDeliveryScript
3✔
644
                newRemoteAddr := msg.SigMsg.CloserScript
3✔
645

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

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

662
                return nil
3✔
663
        }
664

665
        return nil
3✔
666
}
667

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

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

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

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

703
        shouldRouteTo := func(party lntypes.ChannelParty) bool {
6✔
704
                state := c.PeerState.GetForParty(party)
3✔
705
                if state == nil {
3✔
UNCOV
706
                        return false
×
UNCOV
707
                }
×
708

709
                return state.ShouldRouteTo(event)
3✔
710
        }
711

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3✔
901
        return fn.NewResult(sig.UnwrapOrErr(ErrNoSig))
3✔
902
}
903

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1198
        default:
×
1199

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

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

1✔
1211
        return &CloseStateTransition{
1✔
1212
                NextState: c,
1✔
1213
        }, nil
1✔
1214
}
1✔
1215

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

×
UNCOV
1223
        switch msg := event.(type) {
×
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.
UNCOV
1227
        case *SendOfferEvent:
×
UNCOV
1228
                return &CloseStateTransition{
×
UNCOV
1229
                        NextState: &LocalCloseStart{
×
UNCOV
1230
                                CloseChannelTerms: c.CloseChannelTerms,
×
UNCOV
1231
                        },
×
UNCOV
1232
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
×
UNCOV
1233
                                InternalEvent: []ProtocolEvent{msg},
×
UNCOV
1234
                        }),
×
UNCOV
1235
                }, nil
×
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.
UNCOV
1240
        case *OfferReceivedEvent:
×
UNCOV
1241
                return &CloseStateTransition{
×
UNCOV
1242
                        NextState: &RemoteCloseStart{
×
UNCOV
1243
                                CloseChannelTerms: c.CloseChannelTerms,
×
UNCOV
1244
                        },
×
UNCOV
1245
                        NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
×
UNCOV
1246
                                InternalEvent: []ProtocolEvent{msg},
×
UNCOV
1247
                        }),
×
UNCOV
1248
                }, nil
×
1249

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