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

lightningnetwork / lnd / 12312390362

13 Dec 2024 08:44AM UTC coverage: 57.458% (+8.5%) from 48.92%
12312390362

Pull #9343

github

ellemouton
fn: rework the ContextGuard and add tests

In this commit, the ContextGuard struct is re-worked such that the
context that its new main WithCtx method provides is cancelled in sync
with a parent context being cancelled or with it's quit channel being
cancelled. Tests are added to assert the behaviour. In order for the
close of the quit channel to be consistent with the cancelling of the
derived context, the quit channel _must_ be contained internal to the
ContextGuard so that callers are only able to close the channel via the
exposed Quit method which will then take care to first cancel any
derived context that depend on the quit channel before returning.
Pull Request #9343: fn: expand the ContextGuard and add tests

101853 of 177264 relevant lines covered (57.46%)

24972.93 hits per line

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

55.89
/contractcourt/chain_watcher.go
1
package contractcourt
2

3
import (
4
        "bytes"
5
        "fmt"
6
        "slices"
7
        "sync"
8
        "sync/atomic"
9
        "time"
10

11
        "github.com/btcsuite/btcd/btcec/v2"
12
        "github.com/btcsuite/btcd/btcutil"
13
        "github.com/btcsuite/btcd/chaincfg"
14
        "github.com/btcsuite/btcd/chaincfg/chainhash"
15
        "github.com/btcsuite/btcd/mempool"
16
        "github.com/btcsuite/btcd/txscript"
17
        "github.com/btcsuite/btcd/wire"
18
        "github.com/davecgh/go-spew/spew"
19
        "github.com/lightningnetwork/lnd/chainntnfs"
20
        "github.com/lightningnetwork/lnd/channeldb"
21
        "github.com/lightningnetwork/lnd/fn/v2"
22
        "github.com/lightningnetwork/lnd/input"
23
        "github.com/lightningnetwork/lnd/lntypes"
24
        "github.com/lightningnetwork/lnd/lnutils"
25
        "github.com/lightningnetwork/lnd/lnwallet"
26
        "github.com/lightningnetwork/lnd/lnwire"
27
)
28

29
const (
30
        // minCommitPointPollTimeout is the minimum time we'll wait before
31
        // polling the database for a channel's commitpoint.
32
        minCommitPointPollTimeout = 1 * time.Second
33

34
        // maxCommitPointPollTimeout is the maximum time we'll wait before
35
        // polling the database for a channel's commitpoint.
36
        maxCommitPointPollTimeout = 10 * time.Minute
37
)
38

39
// LocalUnilateralCloseInfo encapsulates all the information we need to act on
40
// a local force close that gets confirmed.
41
type LocalUnilateralCloseInfo struct {
42
        *chainntnfs.SpendDetail
43
        *lnwallet.LocalForceCloseSummary
44
        *channeldb.ChannelCloseSummary
45

46
        // CommitSet is the set of known valid commitments at the time the
47
        // remote party's commitment hit the chain.
48
        CommitSet CommitSet
49
}
50

51
// CooperativeCloseInfo encapsulates all the information we need to act on a
52
// cooperative close that gets confirmed.
53
type CooperativeCloseInfo struct {
54
        *channeldb.ChannelCloseSummary
55
}
56

57
// RemoteUnilateralCloseInfo wraps the normal UnilateralCloseSummary to couple
58
// the CommitSet at the time of channel closure.
59
type RemoteUnilateralCloseInfo struct {
60
        *lnwallet.UnilateralCloseSummary
61

62
        // CommitSet is the set of known valid commitments at the time the
63
        // remote party's commitment hit the chain.
64
        CommitSet CommitSet
65
}
66

67
// BreachResolution wraps the outpoint of the breached channel.
68
type BreachResolution struct {
69
        FundingOutPoint wire.OutPoint
70
}
71

72
// BreachCloseInfo wraps the BreachResolution with a CommitSet for the latest,
73
// non-breached state, with the AnchorResolution for the breached state.
74
type BreachCloseInfo struct {
75
        *BreachResolution
76
        *lnwallet.AnchorResolution
77

78
        // CommitHash is the hash of the commitment transaction.
79
        CommitHash chainhash.Hash
80

81
        // CommitSet is the set of known valid commitments at the time the
82
        // breach occurred on-chain.
83
        CommitSet CommitSet
84

85
        // CloseSummary gives the recipient of the BreachCloseInfo information
86
        // to mark the channel closed in the database.
87
        CloseSummary channeldb.ChannelCloseSummary
88
}
89

90
// CommitSet is a collection of the set of known valid commitments at a given
91
// instant. If ConfCommitKey is set, then the commitment identified by the
92
// HtlcSetKey has hit the chain. This struct will be used to examine all live
93
// HTLCs to determine if any additional actions need to be made based on the
94
// remote party's commitments.
95
type CommitSet struct {
96
        // When the ConfCommitKey is set, it signals that the commitment tx was
97
        // confirmed in the chain.
98
        ConfCommitKey fn.Option[HtlcSetKey]
99

100
        // HtlcSets stores the set of all known active HTLC for each active
101
        // commitment at the time of channel closure.
102
        HtlcSets map[HtlcSetKey][]channeldb.HTLC
103
}
104

105
// IsEmpty returns true if there are no HTLCs at all within all commitments
106
// that are a part of this commitment diff.
107
func (c *CommitSet) IsEmpty() bool {
16✔
108
        if c == nil {
16✔
109
                return true
×
110
        }
×
111

112
        for _, htlcs := range c.HtlcSets {
24✔
113
                if len(htlcs) != 0 {
16✔
114
                        return false
8✔
115
                }
8✔
116
        }
117

118
        return true
8✔
119
}
120

121
// toActiveHTLCSets returns the set of all active HTLCs across all commitment
122
// transactions.
123
func (c *CommitSet) toActiveHTLCSets() map[HtlcSetKey]htlcSet {
14✔
124
        htlcSets := make(map[HtlcSetKey]htlcSet)
14✔
125

14✔
126
        for htlcSetKey, htlcs := range c.HtlcSets {
26✔
127
                htlcSets[htlcSetKey] = newHtlcSet(htlcs)
12✔
128
        }
12✔
129

130
        return htlcSets
14✔
131
}
132

133
// ChainEventSubscription is a struct that houses a subscription to be notified
134
// for any on-chain events related to a channel. There are three types of
135
// possible on-chain events: a cooperative channel closure, a unilateral
136
// channel closure, and a channel breach. The fourth type: a force close is
137
// locally initiated, so we don't provide any event stream for said event.
138
type ChainEventSubscription struct {
139
        // ChanPoint is that channel that chain events will be dispatched for.
140
        ChanPoint wire.OutPoint
141

142
        // RemoteUnilateralClosure is a channel that will be sent upon in the
143
        // event that the remote party's commitment transaction is confirmed.
144
        RemoteUnilateralClosure chan *RemoteUnilateralCloseInfo
145

146
        // LocalUnilateralClosure is a channel that will be sent upon in the
147
        // event that our commitment transaction is confirmed.
148
        LocalUnilateralClosure chan *LocalUnilateralCloseInfo
149

150
        // CooperativeClosure is a signal that will be sent upon once a
151
        // cooperative channel closure has been detected confirmed.
152
        CooperativeClosure chan *CooperativeCloseInfo
153

154
        // ContractBreach is a channel that will be sent upon if we detect a
155
        // contract breach. The struct sent across the channel contains all the
156
        // material required to bring the cheating channel peer to justice.
157
        ContractBreach chan *BreachCloseInfo
158

159
        // Cancel cancels the subscription to the event stream for a particular
160
        // channel. This method should be called once the caller no longer needs to
161
        // be notified of any on-chain events for a particular channel.
162
        Cancel func()
163
}
164

165
// chainWatcherConfig encapsulates all the necessary functions and interfaces
166
// needed to watch and act on on-chain events for a particular channel.
167
type chainWatcherConfig struct {
168
        // chanState is a snapshot of the persistent state of the channel that
169
        // we're watching. In the event of an on-chain event, we'll query the
170
        // database to ensure that we act using the most up to date state.
171
        chanState *channeldb.OpenChannel
172

173
        // notifier is a reference to the channel notifier that we'll use to be
174
        // notified of output spends and when transactions are confirmed.
175
        notifier chainntnfs.ChainNotifier
176

177
        // signer is the main signer instances that will be responsible for
178
        // signing any HTLC and commitment transaction generated by the state
179
        // machine.
180
        signer input.Signer
181

182
        // contractBreach is a method that will be called by the watcher if it
183
        // detects that a contract breach transaction has been confirmed. It
184
        // will only return a non-nil error when the BreachArbitrator has
185
        // preserved the necessary breach info for this channel point.
186
        contractBreach func(*lnwallet.BreachRetribution) error
187

188
        // isOurAddr is a function that returns true if the passed address is
189
        // known to us.
190
        isOurAddr func(btcutil.Address) bool
191

192
        // extractStateNumHint extracts the encoded state hint using the passed
193
        // obfuscater. This is used by the chain watcher to identify which
194
        // state was broadcast and confirmed on-chain.
195
        extractStateNumHint func(*wire.MsgTx, [lnwallet.StateHintSize]byte) uint64
196

197
        // auxLeafStore can be used to fetch information for custom channels.
198
        auxLeafStore fn.Option[lnwallet.AuxLeafStore]
199

200
        // auxResolver is used to supplement contract resolution.
201
        auxResolver fn.Option[lnwallet.AuxContractResolver]
202
}
203

204
// chainWatcher is a system that's assigned to every active channel. The duty
205
// of this system is to watch the chain for spends of the channels chan point.
206
// If a spend is detected then with chain watcher will notify all subscribers
207
// that the channel has been closed, and also give them the materials necessary
208
// to sweep the funds of the channel on chain eventually.
209
type chainWatcher struct {
210
        started int32 // To be used atomically.
211
        stopped int32 // To be used atomically.
212

213
        quit chan struct{}
214
        wg   sync.WaitGroup
215

216
        cfg chainWatcherConfig
217

218
        // stateHintObfuscator is a 48-bit state hint that's used to obfuscate
219
        // the current state number on the commitment transactions.
220
        stateHintObfuscator [lnwallet.StateHintSize]byte
221

222
        // fundingPkScript is the pkScript of the funding output.
223
        fundingPkScript []byte
224

225
        // heightHint is the height hint used to checkpoint scans on chain for
226
        // conf/spend events.
227
        heightHint uint32
228

229
        // All the fields below are protected by this mutex.
230
        sync.Mutex
231

232
        // clientID is an ephemeral counter used to keep track of each
233
        // individual client subscription.
234
        clientID uint64
235

236
        // clientSubscriptions is a map that keeps track of all the active
237
        // client subscriptions for events related to this channel.
238
        clientSubscriptions map[uint64]*ChainEventSubscription
239
}
240

241
// newChainWatcher returns a new instance of a chainWatcher for a channel given
242
// the chan point to watch, and also a notifier instance that will allow us to
243
// detect on chain events.
244
func newChainWatcher(cfg chainWatcherConfig) (*chainWatcher, error) {
26✔
245
        // In order to be able to detect the nature of a potential channel
26✔
246
        // closure we'll need to reconstruct the state hint bytes used to
26✔
247
        // obfuscate the commitment state number encoded in the lock time and
26✔
248
        // sequence fields.
26✔
249
        var stateHint [lnwallet.StateHintSize]byte
26✔
250
        chanState := cfg.chanState
26✔
251
        if chanState.IsInitiator {
52✔
252
                stateHint = lnwallet.DeriveStateHintObfuscator(
26✔
253
                        chanState.LocalChanCfg.PaymentBasePoint.PubKey,
26✔
254
                        chanState.RemoteChanCfg.PaymentBasePoint.PubKey,
26✔
255
                )
26✔
256
        } else {
26✔
257
                stateHint = lnwallet.DeriveStateHintObfuscator(
×
258
                        chanState.RemoteChanCfg.PaymentBasePoint.PubKey,
×
259
                        chanState.LocalChanCfg.PaymentBasePoint.PubKey,
×
260
                )
×
261
        }
×
262

263
        return &chainWatcher{
26✔
264
                cfg:                 cfg,
26✔
265
                stateHintObfuscator: stateHint,
26✔
266
                quit:                make(chan struct{}),
26✔
267
                clientSubscriptions: make(map[uint64]*ChainEventSubscription),
26✔
268
        }, nil
26✔
269
}
270

271
// Start starts all goroutines that the chainWatcher needs to perform its
272
// duties.
273
func (c *chainWatcher) Start() error {
26✔
274
        if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
26✔
275
                return nil
×
276
        }
×
277

278
        chanState := c.cfg.chanState
26✔
279
        log.Debugf("Starting chain watcher for ChannelPoint(%v)",
26✔
280
                chanState.FundingOutpoint)
26✔
281

26✔
282
        // First, we'll register for a notification to be dispatched if the
26✔
283
        // funding output is spent.
26✔
284
        fundingOut := &chanState.FundingOutpoint
26✔
285

26✔
286
        // As a height hint, we'll try to use the opening height, but if the
26✔
287
        // channel isn't yet open, then we'll use the height it was broadcast
26✔
288
        // at. This may be an unconfirmed zero-conf channel.
26✔
289
        c.heightHint = c.cfg.chanState.ShortChanID().BlockHeight
26✔
290
        if c.heightHint == 0 {
26✔
291
                c.heightHint = chanState.BroadcastHeight()
×
292
        }
×
293

294
        // Since no zero-conf state is stored in a channel backup, the below
295
        // logic will not be triggered for restored, zero-conf channels. Set
296
        // the height hint for zero-conf channels.
297
        if chanState.IsZeroConf() {
26✔
298
                if chanState.ZeroConfConfirmed() {
×
299
                        // If the zero-conf channel is confirmed, we'll use the
×
300
                        // confirmed SCID's block height.
×
301
                        c.heightHint = chanState.ZeroConfRealScid().BlockHeight
×
302
                } else {
×
303
                        // The zero-conf channel is unconfirmed. We'll need to
×
304
                        // use the FundingBroadcastHeight.
×
305
                        c.heightHint = chanState.BroadcastHeight()
×
306
                }
×
307
        }
308

309
        localKey := chanState.LocalChanCfg.MultiSigKey.PubKey
26✔
310
        remoteKey := chanState.RemoteChanCfg.MultiSigKey.PubKey
26✔
311

26✔
312
        var (
26✔
313
                err error
26✔
314
        )
26✔
315
        if chanState.ChanType.IsTaproot() {
26✔
316
                c.fundingPkScript, _, err = input.GenTaprootFundingScript(
×
317
                        localKey, remoteKey, 0, chanState.TapscriptRoot,
×
318
                )
×
319
                if err != nil {
×
320
                        return err
×
321
                }
×
322
        } else {
26✔
323
                multiSigScript, err := input.GenMultiSigScript(
26✔
324
                        localKey.SerializeCompressed(),
26✔
325
                        remoteKey.SerializeCompressed(),
26✔
326
                )
26✔
327
                if err != nil {
26✔
328
                        return err
×
329
                }
×
330
                c.fundingPkScript, err = input.WitnessScriptHash(multiSigScript)
26✔
331
                if err != nil {
26✔
332
                        return err
×
333
                }
×
334
        }
335

336
        spendNtfn, err := c.cfg.notifier.RegisterSpendNtfn(
26✔
337
                fundingOut, c.fundingPkScript, c.heightHint,
26✔
338
        )
26✔
339
        if err != nil {
26✔
340
                return err
×
341
        }
×
342

343
        // With the spend notification obtained, we'll now dispatch the
344
        // closeObserver which will properly react to any changes.
345
        c.wg.Add(1)
26✔
346
        go c.closeObserver(spendNtfn)
26✔
347

26✔
348
        return nil
26✔
349
}
350

351
// Stop signals the close observer to gracefully exit.
352
func (c *chainWatcher) Stop() error {
26✔
353
        if !atomic.CompareAndSwapInt32(&c.stopped, 0, 1) {
26✔
354
                return nil
×
355
        }
×
356

357
        close(c.quit)
26✔
358

26✔
359
        c.wg.Wait()
26✔
360

26✔
361
        return nil
26✔
362
}
363

364
// SubscribeChannelEvents returns an active subscription to the set of channel
365
// events for the channel watched by this chain watcher. Once clients no longer
366
// require the subscription, they should call the Cancel() method to allow the
367
// watcher to regain those committed resources.
368
func (c *chainWatcher) SubscribeChannelEvents() *ChainEventSubscription {
26✔
369

26✔
370
        c.Lock()
26✔
371
        clientID := c.clientID
26✔
372
        c.clientID++
26✔
373
        c.Unlock()
26✔
374

26✔
375
        log.Debugf("New ChainEventSubscription(id=%v) for ChannelPoint(%v)",
26✔
376
                clientID, c.cfg.chanState.FundingOutpoint)
26✔
377

26✔
378
        sub := &ChainEventSubscription{
26✔
379
                ChanPoint:               c.cfg.chanState.FundingOutpoint,
26✔
380
                RemoteUnilateralClosure: make(chan *RemoteUnilateralCloseInfo, 1),
26✔
381
                LocalUnilateralClosure:  make(chan *LocalUnilateralCloseInfo, 1),
26✔
382
                CooperativeClosure:      make(chan *CooperativeCloseInfo, 1),
26✔
383
                ContractBreach:          make(chan *BreachCloseInfo, 1),
26✔
384
                Cancel: func() {
37✔
385
                        c.Lock()
11✔
386
                        delete(c.clientSubscriptions, clientID)
11✔
387
                        c.Unlock()
11✔
388
                },
11✔
389
        }
390

391
        c.Lock()
26✔
392
        c.clientSubscriptions[clientID] = sub
26✔
393
        c.Unlock()
26✔
394

26✔
395
        return sub
26✔
396
}
397

398
// handleUnknownLocalState checks whether the passed spend _could_ be a local
399
// state that for some reason is unknown to us. This could be a state published
400
// by us before we lost state, which we will try to sweep. Or it could be one
401
// of our revoked states that somehow made it to the chain. If that's the case
402
// we cannot really hope that we'll be able to get our money back, but we'll
403
// try to sweep it anyway. If this is not an unknown local state, false is
404
// returned.
405
func (c *chainWatcher) handleUnknownLocalState(
406
        commitSpend *chainntnfs.SpendDetail, broadcastStateNum uint64,
407
        chainSet *chainSet) (bool, error) {
11✔
408

11✔
409
        // If the spend was a local commitment, at this point it must either be
11✔
410
        // a past state (we breached!) or a future state (we lost state!). In
11✔
411
        // either case, the only thing we can do is to attempt to sweep what is
11✔
412
        // there.
11✔
413

11✔
414
        // First, we'll re-derive our commitment point for this state since
11✔
415
        // this is what we use to randomize each of the keys for this state.
11✔
416
        commitSecret, err := c.cfg.chanState.RevocationProducer.AtIndex(
11✔
417
                broadcastStateNum,
11✔
418
        )
11✔
419
        if err != nil {
11✔
420
                return false, err
×
421
        }
×
422
        commitPoint := input.ComputeCommitmentPoint(commitSecret[:])
11✔
423

11✔
424
        // Now that we have the commit point, we'll derive the tweaked local
11✔
425
        // and remote keys for this state. We use our point as only we can
11✔
426
        // revoke our own commitment.
11✔
427
        commitKeyRing := lnwallet.DeriveCommitmentKeys(
11✔
428
                commitPoint, lntypes.Local, c.cfg.chanState.ChanType,
11✔
429
                &c.cfg.chanState.LocalChanCfg, &c.cfg.chanState.RemoteChanCfg,
11✔
430
        )
11✔
431

11✔
432
        auxResult, err := fn.MapOptionZ(
11✔
433
                c.cfg.auxLeafStore,
11✔
434
                //nolint:ll
11✔
435
                func(s lnwallet.AuxLeafStore) fn.Result[lnwallet.CommitDiffAuxResult] {
11✔
436
                        return s.FetchLeavesFromCommit(
×
437
                                lnwallet.NewAuxChanState(c.cfg.chanState),
×
438
                                c.cfg.chanState.LocalCommitment, *commitKeyRing,
×
439
                                lntypes.Local,
×
440
                        )
×
441
                },
×
442
        ).Unpack()
443
        if err != nil {
11✔
444
                return false, fmt.Errorf("unable to fetch aux leaves: %w", err)
×
445
        }
×
446

447
        // With the keys derived, we'll construct the remote script that'll be
448
        // present if they have a non-dust balance on the commitment.
449
        var leaseExpiry uint32
11✔
450
        if c.cfg.chanState.ChanType.HasLeaseExpiration() {
11✔
451
                leaseExpiry = c.cfg.chanState.ThawHeight
×
452
        }
×
453

454
        remoteAuxLeaf := fn.FlatMapOption(
11✔
455
                func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf {
11✔
456
                        return l.RemoteAuxLeaf
×
457
                },
×
458
        )(auxResult.AuxLeaves)
459
        remoteScript, _, err := lnwallet.CommitScriptToRemote(
11✔
460
                c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
11✔
461
                commitKeyRing.ToRemoteKey, leaseExpiry,
11✔
462
                remoteAuxLeaf,
11✔
463
        )
11✔
464
        if err != nil {
11✔
465
                return false, err
×
466
        }
×
467

468
        // Next, we'll derive our script that includes the revocation base for
469
        // the remote party allowing them to claim this output before the CSV
470
        // delay if we breach.
471
        localAuxLeaf := fn.FlatMapOption(
11✔
472
                func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf {
11✔
473
                        return l.LocalAuxLeaf
×
474
                },
×
475
        )(auxResult.AuxLeaves)
476
        localScript, err := lnwallet.CommitScriptToSelf(
11✔
477
                c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
11✔
478
                commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey,
11✔
479
                uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry,
11✔
480
                localAuxLeaf,
11✔
481
        )
11✔
482
        if err != nil {
11✔
483
                return false, err
×
484
        }
×
485

486
        // With all our scripts assembled, we'll examine the outputs of the
487
        // commitment transaction to determine if this is a local force close
488
        // or not.
489
        ourCommit := false
11✔
490
        for _, output := range commitSpend.SpendingTx.TxOut {
27✔
491
                pkScript := output.PkScript
16✔
492

16✔
493
                switch {
16✔
494
                case bytes.Equal(localScript.PkScript(), pkScript):
4✔
495
                        ourCommit = true
4✔
496

497
                case bytes.Equal(remoteScript.PkScript(), pkScript):
4✔
498
                        ourCommit = true
4✔
499
                }
500
        }
501

502
        // If the script is not present, this cannot be our commit.
503
        if !ourCommit {
15✔
504
                return false, nil
4✔
505
        }
4✔
506

507
        log.Warnf("Detected local unilateral close of unknown state %v "+
7✔
508
                "(our state=%v)", broadcastStateNum,
7✔
509
                chainSet.localCommit.CommitHeight)
7✔
510

7✔
511
        // If this is our commitment transaction, then we try to act even
7✔
512
        // though we won't be able to sweep HTLCs.
7✔
513
        chainSet.commitSet.ConfCommitKey = fn.Some(LocalHtlcSet)
7✔
514
        if err := c.dispatchLocalForceClose(
7✔
515
                commitSpend, broadcastStateNum, chainSet.commitSet,
7✔
516
        ); err != nil {
7✔
517
                return false, fmt.Errorf("unable to handle local"+
×
518
                        "close for chan_point=%v: %v",
×
519
                        c.cfg.chanState.FundingOutpoint, err)
×
520
        }
×
521

522
        return true, nil
7✔
523
}
524

525
// chainSet includes all the information we need to dispatch a channel close
526
// event to any subscribers.
527
type chainSet struct {
528
        // remoteStateNum is the commitment number of the lowest valid
529
        // commitment the remote party holds from our PoV. This value is used
530
        // to determine if the remote party is playing a state that's behind,
531
        // in line, or ahead of the latest state we know for it.
532
        remoteStateNum uint64
533

534
        // commitSet includes information pertaining to the set of active HTLCs
535
        // on each commitment.
536
        commitSet CommitSet
537

538
        // remoteCommit is the current commitment of the remote party.
539
        remoteCommit channeldb.ChannelCommitment
540

541
        // localCommit is our current commitment.
542
        localCommit channeldb.ChannelCommitment
543

544
        // remotePendingCommit points to the dangling commitment of the remote
545
        // party, if it exists. If there's no dangling commitment, then this
546
        // pointer will be nil.
547
        remotePendingCommit *channeldb.ChannelCommitment
548
}
549

550
// newChainSet creates a new chainSet given the current up to date channel
551
// state.
552
func newChainSet(chanState *channeldb.OpenChannel) (*chainSet, error) {
15✔
553
        // First, we'll grab the current unrevoked commitments for ourselves
15✔
554
        // and the remote party.
15✔
555
        localCommit, remoteCommit, err := chanState.LatestCommitments()
15✔
556
        if err != nil {
15✔
557
                return nil, fmt.Errorf("unable to fetch channel state for "+
×
558
                        "chan_point=%v", chanState.FundingOutpoint)
×
559
        }
×
560

561
        log.Tracef("ChannelPoint(%v): local_commit_type=%v, local_commit=%v",
15✔
562
                chanState.FundingOutpoint, chanState.ChanType,
15✔
563
                spew.Sdump(localCommit))
15✔
564
        log.Tracef("ChannelPoint(%v): remote_commit_type=%v, remote_commit=%v",
15✔
565
                chanState.FundingOutpoint, chanState.ChanType,
15✔
566
                spew.Sdump(remoteCommit))
15✔
567

15✔
568
        // Fetch the current known commit height for the remote party, and
15✔
569
        // their pending commitment chain tip if it exists.
15✔
570
        remoteStateNum := remoteCommit.CommitHeight
15✔
571
        remoteChainTip, err := chanState.RemoteCommitChainTip()
15✔
572
        if err != nil && err != channeldb.ErrNoPendingCommit {
15✔
573
                return nil, fmt.Errorf("unable to obtain chain tip for "+
×
574
                        "ChannelPoint(%v): %v",
×
575
                        chanState.FundingOutpoint, err)
×
576
        }
×
577

578
        // Now that we have all the possible valid commitments, we'll make the
579
        // CommitSet the ChannelArbitrator will need in order to carry out its
580
        // duty.
581
        commitSet := CommitSet{
15✔
582
                HtlcSets: map[HtlcSetKey][]channeldb.HTLC{
15✔
583
                        LocalHtlcSet:  localCommit.Htlcs,
15✔
584
                        RemoteHtlcSet: remoteCommit.Htlcs,
15✔
585
                },
15✔
586
        }
15✔
587

15✔
588
        var remotePendingCommit *channeldb.ChannelCommitment
15✔
589
        if remoteChainTip != nil {
16✔
590
                remotePendingCommit = &remoteChainTip.Commitment
1✔
591
                log.Tracef("ChannelPoint(%v): remote_pending_commit_type=%v, "+
1✔
592
                        "remote_pending_commit=%v", chanState.FundingOutpoint,
1✔
593
                        chanState.ChanType,
1✔
594
                        spew.Sdump(remoteChainTip.Commitment))
1✔
595

1✔
596
                htlcs := remoteChainTip.Commitment.Htlcs
1✔
597
                commitSet.HtlcSets[RemotePendingHtlcSet] = htlcs
1✔
598
        }
1✔
599

600
        // We'll now retrieve the latest state of the revocation store so we
601
        // can populate the revocation information within the channel state
602
        // object that we have.
603
        //
604
        // TODO(roasbeef): mutation is bad mkay
605
        _, err = chanState.RemoteRevocationStore()
15✔
606
        if err != nil {
15✔
607
                return nil, fmt.Errorf("unable to fetch revocation state for "+
×
608
                        "chan_point=%v", chanState.FundingOutpoint)
×
609
        }
×
610

611
        return &chainSet{
15✔
612
                remoteStateNum:      remoteStateNum,
15✔
613
                commitSet:           commitSet,
15✔
614
                localCommit:         *localCommit,
15✔
615
                remoteCommit:        *remoteCommit,
15✔
616
                remotePendingCommit: remotePendingCommit,
15✔
617
        }, nil
15✔
618
}
619

620
// closeObserver is a dedicated goroutine that will watch for any closes of the
621
// channel that it's watching on chain. In the event of an on-chain event, the
622
// close observer will assembled the proper materials required to claim the
623
// funds of the channel on-chain (if required), then dispatch these as
624
// notifications to all subscribers.
625
func (c *chainWatcher) closeObserver(spendNtfn *chainntnfs.SpendEvent) {
26✔
626
        defer c.wg.Done()
26✔
627

26✔
628
        log.Infof("Close observer for ChannelPoint(%v) active",
26✔
629
                c.cfg.chanState.FundingOutpoint)
26✔
630

26✔
631
        // If this is a taproot channel, before we proceed, we want to ensure
26✔
632
        // that the expected funding output has confirmed on chain.
26✔
633
        if c.cfg.chanState.ChanType.IsTaproot() {
26✔
634
                fundingPoint := c.cfg.chanState.FundingOutpoint
×
635

×
636
                confNtfn, err := c.cfg.notifier.RegisterConfirmationsNtfn(
×
637
                        &fundingPoint.Hash, c.fundingPkScript, 1, c.heightHint,
×
638
                )
×
639
                if err != nil {
×
640
                        log.Warnf("unable to register for conf: %v", err)
×
641
                }
×
642

643
                log.Infof("Waiting for taproot ChannelPoint(%v) to confirm...",
×
644
                        c.cfg.chanState.FundingOutpoint)
×
645

×
646
                select {
×
647
                case _, ok := <-confNtfn.Confirmed:
×
648
                        // If the channel was closed, then this means that the
×
649
                        // notifier exited, so we will as well.
×
650
                        if !ok {
×
651
                                return
×
652
                        }
×
653
                case <-c.quit:
×
654
                        return
×
655
                }
656
        }
657

658
        select {
26✔
659
        // We've detected a spend of the channel onchain! Depending on the type
660
        // of spend, we'll act accordingly, so we'll examine the spending
661
        // transaction to determine what we should do.
662
        //
663
        // TODO(Roasbeef): need to be able to ensure this only triggers
664
        // on confirmation, to ensure if multiple txns are broadcast, we
665
        // act on the one that's timestamped
666
        case commitSpend, ok := <-spendNtfn.Spend:
15✔
667
                // If the channel was closed, then this means that the notifier
15✔
668
                // exited, so we will as well.
15✔
669
                if !ok {
15✔
670
                        return
×
671
                }
×
672

673
                // Otherwise, the remote party might have broadcast a prior
674
                // revoked state...!!!
675
                commitTxBroadcast := commitSpend.SpendingTx
15✔
676

15✔
677
                // First, we'll construct the chainset which includes all the
15✔
678
                // data we need to dispatch an event to our subscribers about
15✔
679
                // this possible channel close event.
15✔
680
                chainSet, err := newChainSet(c.cfg.chanState)
15✔
681
                if err != nil {
15✔
682
                        log.Errorf("unable to create commit set: %v", err)
×
683
                        return
×
684
                }
×
685

686
                // Decode the state hint encoded within the commitment
687
                // transaction to determine if this is a revoked state or not.
688
                obfuscator := c.stateHintObfuscator
15✔
689
                broadcastStateNum := c.cfg.extractStateNumHint(
15✔
690
                        commitTxBroadcast, obfuscator,
15✔
691
                )
15✔
692

15✔
693
                // We'll go on to check whether it could be our own commitment
15✔
694
                // that was published and know is confirmed.
15✔
695
                ok, err = c.handleKnownLocalState(
15✔
696
                        commitSpend, broadcastStateNum, chainSet,
15✔
697
                )
15✔
698
                if err != nil {
15✔
699
                        log.Errorf("Unable to handle known local state: %v",
×
700
                                err)
×
701
                        return
×
702
                }
×
703

704
                if ok {
17✔
705
                        return
2✔
706
                }
2✔
707

708
                // Now that we know it is neither a non-cooperative closure nor
709
                // a local close with the latest state, we check if it is the
710
                // remote that closed with any prior or current state.
711
                ok, err = c.handleKnownRemoteState(
13✔
712
                        commitSpend, broadcastStateNum, chainSet,
13✔
713
                )
13✔
714
                if err != nil {
13✔
715
                        log.Errorf("Unable to handle known remote state: %v",
×
716
                                err)
×
717
                        return
×
718
                }
×
719

720
                if ok {
15✔
721
                        return
2✔
722
                }
2✔
723

724
                // Next, we'll check to see if this is a cooperative channel
725
                // closure or not. This is characterized by having an input
726
                // sequence number that's finalized. This won't happen with
727
                // regular commitment transactions due to the state hint
728
                // encoding scheme.
729
                switch commitTxBroadcast.TxIn[0].Sequence {
11✔
730
                case wire.MaxTxInSequenceNum:
×
731
                        fallthrough
×
732
                case mempool.MaxRBFSequence:
×
733
                        // TODO(roasbeef): rare but possible, need itest case
×
734
                        // for
×
735
                        err := c.dispatchCooperativeClose(commitSpend)
×
736
                        if err != nil {
×
737
                                log.Errorf("unable to handle co op close: %v", err)
×
738
                        }
×
739
                        return
×
740
                }
741

742
                log.Warnf("Unknown commitment broadcast for "+
11✔
743
                        "ChannelPoint(%v) ", c.cfg.chanState.FundingOutpoint)
11✔
744

11✔
745
                // We'll try to recover as best as possible from losing state.
11✔
746
                // We first check if this was a local unknown state. This could
11✔
747
                // happen if we force close, then lose state or attempt
11✔
748
                // recovery before the commitment confirms.
11✔
749
                ok, err = c.handleUnknownLocalState(
11✔
750
                        commitSpend, broadcastStateNum, chainSet,
11✔
751
                )
11✔
752
                if err != nil {
11✔
753
                        log.Errorf("Unable to handle known local state: %v",
×
754
                                err)
×
755
                        return
×
756
                }
×
757

758
                if ok {
18✔
759
                        return
7✔
760
                }
7✔
761

762
                // Since it was neither a known remote state, nor a local state
763
                // that was published, it most likely mean we lost state and
764
                // the remote node closed. In this case we must start the DLP
765
                // protocol in hope of getting our money back.
766
                ok, err = c.handleUnknownRemoteState(
4✔
767
                        commitSpend, broadcastStateNum, chainSet,
4✔
768
                )
4✔
769
                if err != nil {
4✔
770
                        log.Errorf("Unable to handle unknown remote state: %v",
×
771
                                err)
×
772
                        return
×
773
                }
×
774

775
                if ok {
8✔
776
                        return
4✔
777
                }
4✔
778

779
                log.Warnf("Unable to handle spending tx %v of channel point %v",
×
780
                        commitTxBroadcast.TxHash(), c.cfg.chanState.FundingOutpoint)
×
781
                return
×
782

783
        // The chainWatcher has been signalled to exit, so we'll do so now.
784
        case <-c.quit:
11✔
785
                return
11✔
786
        }
787
}
788

789
// handleKnownLocalState checks whether the passed spend is a local state that
790
// is known to us (the current state). If so we will act on this state using
791
// the passed chainSet. If this is not a known local state, false is returned.
792
func (c *chainWatcher) handleKnownLocalState(
793
        commitSpend *chainntnfs.SpendDetail, broadcastStateNum uint64,
794
        chainSet *chainSet) (bool, error) {
15✔
795

15✔
796
        // If the channel is recovered, we won't have a local commit to check
15✔
797
        // against, so immediately return.
15✔
798
        if c.cfg.chanState.HasChanStatus(channeldb.ChanStatusRestored) {
15✔
799
                return false, nil
×
800
        }
×
801

802
        commitTxBroadcast := commitSpend.SpendingTx
15✔
803
        commitHash := commitTxBroadcast.TxHash()
15✔
804

15✔
805
        // Check whether our latest local state hit the chain.
15✔
806
        if chainSet.localCommit.CommitTx.TxHash() != commitHash {
28✔
807
                return false, nil
13✔
808
        }
13✔
809

810
        chainSet.commitSet.ConfCommitKey = fn.Some(LocalHtlcSet)
2✔
811
        if err := c.dispatchLocalForceClose(
2✔
812
                commitSpend, broadcastStateNum, chainSet.commitSet,
2✔
813
        ); err != nil {
2✔
814
                return false, fmt.Errorf("unable to handle local"+
×
815
                        "close for chan_point=%v: %v",
×
816
                        c.cfg.chanState.FundingOutpoint, err)
×
817
        }
×
818

819
        return true, nil
2✔
820
}
821

822
// handleKnownRemoteState checks whether the passed spend is a remote state
823
// that is known to us (a revoked, current or pending state). If so we will act
824
// on this state using the passed chainSet. If this is not a known remote
825
// state, false is returned.
826
func (c *chainWatcher) handleKnownRemoteState(
827
        commitSpend *chainntnfs.SpendDetail, broadcastStateNum uint64,
828
        chainSet *chainSet) (bool, error) {
13✔
829

13✔
830
        // If the channel is recovered, we won't have any remote commit to
13✔
831
        // check against, so imemdiately return.
13✔
832
        if c.cfg.chanState.HasChanStatus(channeldb.ChanStatusRestored) {
13✔
833
                return false, nil
×
834
        }
×
835

836
        commitTxBroadcast := commitSpend.SpendingTx
13✔
837
        commitHash := commitTxBroadcast.TxHash()
13✔
838

13✔
839
        switch {
13✔
840
        // If the spending transaction matches the current latest state, then
841
        // they've initiated a unilateral close. So we'll trigger the
842
        // unilateral close signal so subscribers can clean up the state as
843
        // necessary.
844
        case chainSet.remoteCommit.CommitTx.TxHash() == commitHash:
1✔
845
                log.Infof("Remote party broadcast base set, "+
1✔
846
                        "commit_num=%v", chainSet.remoteStateNum)
1✔
847

1✔
848
                chainSet.commitSet.ConfCommitKey = fn.Some(RemoteHtlcSet)
1✔
849
                err := c.dispatchRemoteForceClose(
1✔
850
                        commitSpend, chainSet.remoteCommit,
1✔
851
                        chainSet.commitSet,
1✔
852
                        c.cfg.chanState.RemoteCurrentRevocation,
1✔
853
                )
1✔
854
                if err != nil {
1✔
855
                        return false, fmt.Errorf("unable to handle remote "+
×
856
                                "close for chan_point=%v: %v",
×
857
                                c.cfg.chanState.FundingOutpoint, err)
×
858
                }
×
859

860
                return true, nil
1✔
861

862
        // We'll also handle the case of the remote party broadcasting
863
        // their commitment transaction which is one height above ours.
864
        // This case can arise when we initiate a state transition, but
865
        // the remote party has a fail crash _after_ accepting the new
866
        // state, but _before_ sending their signature to us.
867
        case chainSet.remotePendingCommit != nil &&
868
                chainSet.remotePendingCommit.CommitTx.TxHash() == commitHash:
1✔
869

1✔
870
                log.Infof("Remote party broadcast pending set, "+
1✔
871
                        "commit_num=%v", chainSet.remoteStateNum+1)
1✔
872

1✔
873
                chainSet.commitSet.ConfCommitKey = fn.Some(RemotePendingHtlcSet)
1✔
874
                err := c.dispatchRemoteForceClose(
1✔
875
                        commitSpend, *chainSet.remotePendingCommit,
1✔
876
                        chainSet.commitSet,
1✔
877
                        c.cfg.chanState.RemoteNextRevocation,
1✔
878
                )
1✔
879
                if err != nil {
1✔
880
                        return false, fmt.Errorf("unable to handle remote "+
×
881
                                "close for chan_point=%v: %v",
×
882
                                c.cfg.chanState.FundingOutpoint, err)
×
883
                }
×
884

885
                return true, nil
1✔
886
        }
887

888
        // This is neither a remote force close or a "future" commitment, we
889
        // now check whether it's a remote breach and properly handle it.
890
        return c.handlePossibleBreach(commitSpend, broadcastStateNum, chainSet)
11✔
891
}
892

893
// handlePossibleBreach checks whether the remote has breached and dispatches a
894
// breach resolution to claim funds.
895
func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail,
896
        broadcastStateNum uint64, chainSet *chainSet) (bool, error) {
11✔
897

11✔
898
        // We check if we have a revoked state at this state num that matches
11✔
899
        // the spend transaction.
11✔
900
        spendHeight := uint32(commitSpend.SpendingHeight)
11✔
901
        retribution, err := lnwallet.NewBreachRetribution(
11✔
902
                c.cfg.chanState, broadcastStateNum, spendHeight,
11✔
903
                commitSpend.SpendingTx, c.cfg.auxLeafStore, c.cfg.auxResolver,
11✔
904
        )
11✔
905

11✔
906
        switch {
11✔
907
        // If we had no log entry at this height, this was not a revoked state.
908
        case err == channeldb.ErrLogEntryNotFound:
8✔
909
                return false, nil
8✔
910
        case err == channeldb.ErrNoPastDeltas:
3✔
911
                return false, nil
3✔
912

913
        case err != nil:
×
914
                return false, fmt.Errorf("unable to create breach "+
×
915
                        "retribution: %v", err)
×
916
        }
917

918
        // We found a revoked state at this height, but it could still be our
919
        // own broadcasted state we are looking at. Therefore check that the
920
        // commit matches before assuming it was a breach.
921
        commitHash := commitSpend.SpendingTx.TxHash()
×
922
        if retribution.BreachTxHash != commitHash {
×
923
                return false, nil
×
924
        }
×
925

926
        // Create an AnchorResolution for the breached state.
927
        anchorRes, err := lnwallet.NewAnchorResolution(
×
928
                c.cfg.chanState, commitSpend.SpendingTx, retribution.KeyRing,
×
929
                lntypes.Remote,
×
930
        )
×
931
        if err != nil {
×
932
                return false, fmt.Errorf("unable to create anchor "+
×
933
                        "resolution: %v", err)
×
934
        }
×
935

936
        // We'll set the ConfCommitKey here as the remote htlc set. This is
937
        // only used to ensure a nil-pointer-dereference doesn't occur and is
938
        // not used otherwise. The HTLC's may not exist for the
939
        // RemotePendingHtlcSet.
940
        chainSet.commitSet.ConfCommitKey = fn.Some(RemoteHtlcSet)
×
941

×
942
        // THEY'RE ATTEMPTING TO VIOLATE THE CONTRACT LAID OUT WITHIN THE
×
943
        // PAYMENT CHANNEL. Therefore we close the signal indicating a revoked
×
944
        // broadcast to allow subscribers to swiftly dispatch justice!!!
×
945
        err = c.dispatchContractBreach(
×
946
                commitSpend, chainSet, broadcastStateNum, retribution,
×
947
                anchorRes,
×
948
        )
×
949
        if err != nil {
×
950
                return false, fmt.Errorf("unable to handle channel "+
×
951
                        "breach for chan_point=%v: %v",
×
952
                        c.cfg.chanState.FundingOutpoint, err)
×
953
        }
×
954

955
        return true, nil
×
956
}
957

958
// handleUnknownRemoteState is the last attempt we make at reclaiming funds
959
// from the closed channel, by checkin whether the passed spend _could_ be a
960
// remote spend that is unknown to us (we lost state). We will try to initiate
961
// Data Loss Protection in order to restore our commit point and reclaim our
962
// funds from the channel. If we are not able to act on it, false is returned.
963
func (c *chainWatcher) handleUnknownRemoteState(
964
        commitSpend *chainntnfs.SpendDetail, broadcastStateNum uint64,
965
        chainSet *chainSet) (bool, error) {
4✔
966

4✔
967
        log.Warnf("Remote node broadcast state #%v, "+
4✔
968
                "which is more than 1 beyond best known "+
4✔
969
                "state #%v!!! Attempting recovery...",
4✔
970
                broadcastStateNum, chainSet.remoteStateNum)
4✔
971

4✔
972
        // If this isn't a tweakless commitment, then we'll need to wait for
4✔
973
        // the remote party's latest unrevoked commitment point to be presented
4✔
974
        // to us as we need this to sweep. Otherwise, we can dispatch the
4✔
975
        // remote close and sweep immediately using a fake commitPoint as it
4✔
976
        // isn't actually needed for recovery anymore.
4✔
977
        commitPoint := c.cfg.chanState.RemoteCurrentRevocation
4✔
978
        tweaklessCommit := c.cfg.chanState.ChanType.IsTweakless()
4✔
979
        if !tweaklessCommit {
8✔
980
                commitPoint = c.waitForCommitmentPoint()
4✔
981
                if commitPoint == nil {
4✔
982
                        return false, fmt.Errorf("unable to get commit point")
×
983
                }
×
984

985
                log.Infof("Recovered commit point(%x) for "+
4✔
986
                        "channel(%v)! Now attempting to use it to "+
4✔
987
                        "sweep our funds...",
4✔
988
                        commitPoint.SerializeCompressed(),
4✔
989
                        c.cfg.chanState.FundingOutpoint)
4✔
990
        } else {
×
991
                log.Infof("ChannelPoint(%v) is tweakless, "+
×
992
                        "moving to sweep directly on chain",
×
993
                        c.cfg.chanState.FundingOutpoint)
×
994
        }
×
995

996
        // Since we don't have the commitment stored for this state, we'll just
997
        // pass an empty commitment within the commitment set. Note that this
998
        // means we won't be able to recover any HTLC funds.
999
        //
1000
        // TODO(halseth): can we try to recover some HTLCs?
1001
        chainSet.commitSet.ConfCommitKey = fn.Some(RemoteHtlcSet)
4✔
1002
        err := c.dispatchRemoteForceClose(
4✔
1003
                commitSpend, channeldb.ChannelCommitment{},
4✔
1004
                chainSet.commitSet, commitPoint,
4✔
1005
        )
4✔
1006
        if err != nil {
4✔
1007
                return false, fmt.Errorf("unable to handle remote "+
×
1008
                        "close for chan_point=%v: %v",
×
1009
                        c.cfg.chanState.FundingOutpoint, err)
×
1010
        }
×
1011

1012
        return true, nil
4✔
1013
}
1014

1015
// toSelfAmount takes a transaction and returns the sum of all outputs that pay
1016
// to a script that the wallet controls or the channel defines as its delivery
1017
// script . If no outputs pay to us (determined by these criteria), then we
1018
// return zero. This is possible as our output may have been trimmed due to
1019
// being dust.
1020
func (c *chainWatcher) toSelfAmount(tx *wire.MsgTx) btcutil.Amount {
×
1021
        // There are two main cases we have to handle here. First, in the coop
×
1022
        // close case we will always have saved the delivery address we used
×
1023
        // whether it was from the upfront shutdown, from the delivery address
×
1024
        // requested at close time, or even an automatically generated one. All
×
1025
        // coop-close cases can be identified in the following manner:
×
1026
        shutdown, _ := c.cfg.chanState.ShutdownInfo()
×
1027
        oDeliveryAddr := fn.MapOption(
×
1028
                func(i channeldb.ShutdownInfo) lnwire.DeliveryAddress {
×
1029
                        return i.DeliveryScript.Val
×
1030
                })(shutdown)
×
1031

1032
        // Here we define a function capable of identifying whether an output
1033
        // corresponds with our local delivery script from a ShutdownInfo if we
1034
        // have a ShutdownInfo for this chainWatcher's underlying channel.
1035
        //
1036
        // isDeliveryOutput :: *TxOut -> bool
1037
        isDeliveryOutput := func(o *wire.TxOut) bool {
×
1038
                return fn.ElimOption(
×
1039
                        oDeliveryAddr,
×
1040
                        // If we don't have a delivery addr, then the output
×
1041
                        // can't match it.
×
1042
                        func() bool { return false },
×
1043
                        // Otherwise if the PkScript of the TxOut matches our
1044
                        // delivery script then this is a delivery output.
1045
                        func(a lnwire.DeliveryAddress) bool {
×
1046
                                return slices.Equal(a, o.PkScript)
×
1047
                        },
×
1048
                )
1049
        }
1050

1051
        // Here we define a function capable of identifying whether an output
1052
        // belongs to the LND wallet. We use this as a heuristic in the case
1053
        // where we might be looking for spendable force closure outputs.
1054
        //
1055
        // isWalletOutput :: *TxOut -> bool
1056
        isWalletOutput := func(out *wire.TxOut) bool {
×
1057
                _, addrs, _, err := txscript.ExtractPkScriptAddrs(
×
1058
                        // Doesn't matter what net we actually pass in.
×
1059
                        out.PkScript, &chaincfg.TestNet3Params,
×
1060
                )
×
1061
                if err != nil {
×
1062
                        return false
×
1063
                }
×
1064

1065
                return fn.Any(addrs, c.cfg.isOurAddr)
×
1066
        }
1067

1068
        // Grab all of the outputs that correspond with our delivery address
1069
        // or our wallet is aware of.
1070
        outs := fn.Filter(tx.TxOut, fn.PredOr(isDeliveryOutput, isWalletOutput))
×
1071

×
1072
        // Grab the values for those outputs.
×
1073
        vals := fn.Map(outs, func(o *wire.TxOut) int64 { return o.Value })
×
1074

1075
        // Return the sum.
1076
        return btcutil.Amount(fn.Sum(vals))
×
1077
}
1078

1079
// dispatchCooperativeClose processed a detect cooperative channel closure.
1080
// We'll use the spending transaction to locate our output within the
1081
// transaction, then clean up the database state. We'll also dispatch a
1082
// notification to all subscribers that the channel has been closed in this
1083
// manner.
1084
func (c *chainWatcher) dispatchCooperativeClose(commitSpend *chainntnfs.SpendDetail) error {
×
1085
        broadcastTx := commitSpend.SpendingTx
×
1086

×
1087
        log.Infof("Cooperative closure for ChannelPoint(%v): %v",
×
1088
                c.cfg.chanState.FundingOutpoint, spew.Sdump(broadcastTx))
×
1089

×
1090
        // If the input *is* final, then we'll check to see which output is
×
1091
        // ours.
×
1092
        localAmt := c.toSelfAmount(broadcastTx)
×
1093

×
1094
        // Once this is known, we'll mark the state as fully closed in the
×
1095
        // database. We can do this as a cooperatively closed channel has all
×
1096
        // its outputs resolved after only one confirmation.
×
1097
        closeSummary := &channeldb.ChannelCloseSummary{
×
1098
                ChanPoint:               c.cfg.chanState.FundingOutpoint,
×
1099
                ChainHash:               c.cfg.chanState.ChainHash,
×
1100
                ClosingTXID:             *commitSpend.SpenderTxHash,
×
1101
                RemotePub:               c.cfg.chanState.IdentityPub,
×
1102
                Capacity:                c.cfg.chanState.Capacity,
×
1103
                CloseHeight:             uint32(commitSpend.SpendingHeight),
×
1104
                SettledBalance:          localAmt,
×
1105
                CloseType:               channeldb.CooperativeClose,
×
1106
                ShortChanID:             c.cfg.chanState.ShortChanID(),
×
1107
                IsPending:               true,
×
1108
                RemoteCurrentRevocation: c.cfg.chanState.RemoteCurrentRevocation,
×
1109
                RemoteNextRevocation:    c.cfg.chanState.RemoteNextRevocation,
×
1110
                LocalChanConfig:         c.cfg.chanState.LocalChanCfg,
×
1111
        }
×
1112

×
1113
        // Attempt to add a channel sync message to the close summary.
×
1114
        chanSync, err := c.cfg.chanState.ChanSyncMsg()
×
1115
        if err != nil {
×
1116
                log.Errorf("ChannelPoint(%v): unable to create channel sync "+
×
1117
                        "message: %v", c.cfg.chanState.FundingOutpoint, err)
×
1118
        } else {
×
1119
                closeSummary.LastChanSyncMsg = chanSync
×
1120
        }
×
1121

1122
        // Create a summary of all the information needed to handle the
1123
        // cooperative closure.
1124
        closeInfo := &CooperativeCloseInfo{
×
1125
                ChannelCloseSummary: closeSummary,
×
1126
        }
×
1127

×
1128
        // With the event processed, we'll now notify all subscribers of the
×
1129
        // event.
×
1130
        c.Lock()
×
1131
        for _, sub := range c.clientSubscriptions {
×
1132
                select {
×
1133
                case sub.CooperativeClosure <- closeInfo:
×
1134
                case <-c.quit:
×
1135
                        c.Unlock()
×
1136
                        return fmt.Errorf("exiting")
×
1137
                }
1138
        }
1139
        c.Unlock()
×
1140

×
1141
        return nil
×
1142
}
1143

1144
// dispatchLocalForceClose processes a unilateral close by us being confirmed.
1145
func (c *chainWatcher) dispatchLocalForceClose(
1146
        commitSpend *chainntnfs.SpendDetail,
1147
        stateNum uint64, commitSet CommitSet) error {
9✔
1148

9✔
1149
        log.Infof("Local unilateral close of ChannelPoint(%v) "+
9✔
1150
                "detected", c.cfg.chanState.FundingOutpoint)
9✔
1151

9✔
1152
        forceClose, err := lnwallet.NewLocalForceCloseSummary(
9✔
1153
                c.cfg.chanState, c.cfg.signer, commitSpend.SpendingTx, stateNum,
9✔
1154
                c.cfg.auxLeafStore, c.cfg.auxResolver,
9✔
1155
        )
9✔
1156
        if err != nil {
9✔
1157
                return err
×
1158
        }
×
1159

1160
        // As we've detected that the channel has been closed, immediately
1161
        // creating a close summary for future usage by related sub-systems.
1162
        chanSnapshot := forceClose.ChanSnapshot
9✔
1163
        closeSummary := &channeldb.ChannelCloseSummary{
9✔
1164
                ChanPoint:               chanSnapshot.ChannelPoint,
9✔
1165
                ChainHash:               chanSnapshot.ChainHash,
9✔
1166
                ClosingTXID:             forceClose.CloseTx.TxHash(),
9✔
1167
                RemotePub:               &chanSnapshot.RemoteIdentity,
9✔
1168
                Capacity:                chanSnapshot.Capacity,
9✔
1169
                CloseType:               channeldb.LocalForceClose,
9✔
1170
                IsPending:               true,
9✔
1171
                ShortChanID:             c.cfg.chanState.ShortChanID(),
9✔
1172
                CloseHeight:             uint32(commitSpend.SpendingHeight),
9✔
1173
                RemoteCurrentRevocation: c.cfg.chanState.RemoteCurrentRevocation,
9✔
1174
                RemoteNextRevocation:    c.cfg.chanState.RemoteNextRevocation,
9✔
1175
                LocalChanConfig:         c.cfg.chanState.LocalChanCfg,
9✔
1176
        }
9✔
1177

9✔
1178
        resolutions, err := forceClose.ContractResolutions.UnwrapOrErr(
9✔
1179
                fmt.Errorf("resolutions not found"),
9✔
1180
        )
9✔
1181
        if err != nil {
9✔
1182
                return err
×
1183
        }
×
1184

1185
        // If our commitment output isn't dust or we have active HTLC's on the
1186
        // commitment transaction, then we'll populate the balances on the
1187
        // close channel summary.
1188
        if resolutions.CommitResolution != nil {
15✔
1189
                localBalance := chanSnapshot.LocalBalance.ToSatoshis()
6✔
1190
                closeSummary.SettledBalance = localBalance
6✔
1191
                closeSummary.TimeLockedBalance = localBalance
6✔
1192
        }
6✔
1193

1194
        if resolutions.HtlcResolutions != nil {
18✔
1195
                for _, htlc := range resolutions.HtlcResolutions.OutgoingHTLCs {
9✔
1196
                        htlcValue := btcutil.Amount(
×
1197
                                htlc.SweepSignDesc.Output.Value,
×
1198
                        )
×
1199
                        closeSummary.TimeLockedBalance += htlcValue
×
1200
                }
×
1201
        }
1202

1203
        // Attempt to add a channel sync message to the close summary.
1204
        chanSync, err := c.cfg.chanState.ChanSyncMsg()
9✔
1205
        if err != nil {
9✔
1206
                log.Errorf("ChannelPoint(%v): unable to create channel sync "+
×
1207
                        "message: %v", c.cfg.chanState.FundingOutpoint, err)
×
1208
        } else {
9✔
1209
                closeSummary.LastChanSyncMsg = chanSync
9✔
1210
        }
9✔
1211

1212
        // With the event processed, we'll now notify all subscribers of the
1213
        // event.
1214
        closeInfo := &LocalUnilateralCloseInfo{
9✔
1215
                SpendDetail:            commitSpend,
9✔
1216
                LocalForceCloseSummary: forceClose,
9✔
1217
                ChannelCloseSummary:    closeSummary,
9✔
1218
                CommitSet:              commitSet,
9✔
1219
        }
9✔
1220
        c.Lock()
9✔
1221
        for _, sub := range c.clientSubscriptions {
18✔
1222
                select {
9✔
1223
                case sub.LocalUnilateralClosure <- closeInfo:
9✔
1224
                case <-c.quit:
×
1225
                        c.Unlock()
×
1226
                        return fmt.Errorf("exiting")
×
1227
                }
1228
        }
1229
        c.Unlock()
9✔
1230

9✔
1231
        return nil
9✔
1232
}
1233

1234
// dispatchRemoteForceClose processes a detected unilateral channel closure by
1235
// the remote party. This function will prepare a UnilateralCloseSummary which
1236
// will then be sent to any subscribers allowing them to resolve all our funds
1237
// in the channel on chain. Once this close summary is prepared, all registered
1238
// subscribers will receive a notification of this event. The commitPoint
1239
// argument should be set to the per_commitment_point corresponding to the
1240
// spending commitment.
1241
//
1242
// NOTE: The remoteCommit argument should be set to the stored commitment for
1243
// this particular state. If we don't have the commitment stored (should only
1244
// happen in case we have lost state) it should be set to an empty struct, in
1245
// which case we will attempt to sweep the non-HTLC output using the passed
1246
// commitPoint.
1247
func (c *chainWatcher) dispatchRemoteForceClose(
1248
        commitSpend *chainntnfs.SpendDetail,
1249
        remoteCommit channeldb.ChannelCommitment,
1250
        commitSet CommitSet, commitPoint *btcec.PublicKey) error {
6✔
1251

6✔
1252
        log.Infof("Unilateral close of ChannelPoint(%v) "+
6✔
1253
                "detected", c.cfg.chanState.FundingOutpoint)
6✔
1254

6✔
1255
        // First, we'll create a closure summary that contains all the
6✔
1256
        // materials required to let each subscriber sweep the funds in the
6✔
1257
        // channel on-chain.
6✔
1258
        uniClose, err := lnwallet.NewUnilateralCloseSummary(
6✔
1259
                c.cfg.chanState, c.cfg.signer, commitSpend, remoteCommit,
6✔
1260
                commitPoint, c.cfg.auxLeafStore, c.cfg.auxResolver,
6✔
1261
        )
6✔
1262
        if err != nil {
6✔
1263
                return err
×
1264
        }
×
1265

1266
        // With the event processed, we'll now notify all subscribers of the
1267
        // event.
1268
        c.Lock()
6✔
1269
        for _, sub := range c.clientSubscriptions {
12✔
1270
                select {
6✔
1271
                case sub.RemoteUnilateralClosure <- &RemoteUnilateralCloseInfo{
1272
                        UnilateralCloseSummary: uniClose,
1273
                        CommitSet:              commitSet,
1274
                }:
6✔
1275
                case <-c.quit:
×
1276
                        c.Unlock()
×
1277
                        return fmt.Errorf("exiting")
×
1278
                }
1279
        }
1280
        c.Unlock()
6✔
1281

6✔
1282
        return nil
6✔
1283
}
1284

1285
// dispatchContractBreach processes a detected contract breached by the remote
1286
// party. This method is to be called once we detect that the remote party has
1287
// broadcast a prior revoked commitment state. This method well prepare all the
1288
// materials required to bring the cheater to justice, then notify all
1289
// registered subscribers of this event.
1290
func (c *chainWatcher) dispatchContractBreach(spendEvent *chainntnfs.SpendDetail,
1291
        chainSet *chainSet, broadcastStateNum uint64,
1292
        retribution *lnwallet.BreachRetribution,
1293
        anchorRes *lnwallet.AnchorResolution) error {
×
1294

×
1295
        log.Warnf("Remote peer has breached the channel contract for "+
×
1296
                "ChannelPoint(%v). Revoked state #%v was broadcast!!!",
×
1297
                c.cfg.chanState.FundingOutpoint, broadcastStateNum)
×
1298

×
1299
        if err := c.cfg.chanState.MarkBorked(); err != nil {
×
1300
                return fmt.Errorf("unable to mark channel as borked: %w", err)
×
1301
        }
×
1302

1303
        spendHeight := uint32(spendEvent.SpendingHeight)
×
1304

×
1305
        log.Debugf("Punishment breach retribution created: %v",
×
1306
                lnutils.NewLogClosure(func() string {
×
1307
                        retribution.KeyRing.LocalHtlcKey = nil
×
1308
                        retribution.KeyRing.RemoteHtlcKey = nil
×
1309
                        retribution.KeyRing.ToLocalKey = nil
×
1310
                        retribution.KeyRing.ToRemoteKey = nil
×
1311
                        retribution.KeyRing.RevocationKey = nil
×
1312
                        return spew.Sdump(retribution)
×
1313
                }))
×
1314

1315
        settledBalance := chainSet.remoteCommit.LocalBalance.ToSatoshis()
×
1316
        closeSummary := channeldb.ChannelCloseSummary{
×
1317
                ChanPoint:               c.cfg.chanState.FundingOutpoint,
×
1318
                ChainHash:               c.cfg.chanState.ChainHash,
×
1319
                ClosingTXID:             *spendEvent.SpenderTxHash,
×
1320
                CloseHeight:             spendHeight,
×
1321
                RemotePub:               c.cfg.chanState.IdentityPub,
×
1322
                Capacity:                c.cfg.chanState.Capacity,
×
1323
                SettledBalance:          settledBalance,
×
1324
                CloseType:               channeldb.BreachClose,
×
1325
                IsPending:               true,
×
1326
                ShortChanID:             c.cfg.chanState.ShortChanID(),
×
1327
                RemoteCurrentRevocation: c.cfg.chanState.RemoteCurrentRevocation,
×
1328
                RemoteNextRevocation:    c.cfg.chanState.RemoteNextRevocation,
×
1329
                LocalChanConfig:         c.cfg.chanState.LocalChanCfg,
×
1330
        }
×
1331

×
1332
        // Attempt to add a channel sync message to the close summary.
×
1333
        chanSync, err := c.cfg.chanState.ChanSyncMsg()
×
1334
        if err != nil {
×
1335
                log.Errorf("ChannelPoint(%v): unable to create channel sync "+
×
1336
                        "message: %v", c.cfg.chanState.FundingOutpoint, err)
×
1337
        } else {
×
1338
                closeSummary.LastChanSyncMsg = chanSync
×
1339
        }
×
1340

1341
        // Hand the retribution info over to the BreachArbitrator. This function
1342
        // will wait for a response from the breach arbiter and then proceed to
1343
        // send a BreachCloseInfo to the channel arbitrator. The channel arb
1344
        // will then mark the channel as closed after resolutions and the
1345
        // commit set are logged in the arbitrator log.
1346
        if err := c.cfg.contractBreach(retribution); err != nil {
×
1347
                log.Errorf("unable to hand breached contract off to "+
×
1348
                        "BreachArbitrator: %v", err)
×
1349
                return err
×
1350
        }
×
1351

1352
        breachRes := &BreachResolution{
×
1353
                FundingOutPoint: c.cfg.chanState.FundingOutpoint,
×
1354
        }
×
1355

×
1356
        breachInfo := &BreachCloseInfo{
×
1357
                CommitHash:       spendEvent.SpendingTx.TxHash(),
×
1358
                BreachResolution: breachRes,
×
1359
                AnchorResolution: anchorRes,
×
1360
                CommitSet:        chainSet.commitSet,
×
1361
                CloseSummary:     closeSummary,
×
1362
        }
×
1363

×
1364
        // With the event processed and channel closed, we'll now notify all
×
1365
        // subscribers of the event.
×
1366
        c.Lock()
×
1367
        for _, sub := range c.clientSubscriptions {
×
1368
                select {
×
1369
                case sub.ContractBreach <- breachInfo:
×
1370
                case <-c.quit:
×
1371
                        c.Unlock()
×
1372
                        return fmt.Errorf("quitting")
×
1373
                }
1374
        }
1375
        c.Unlock()
×
1376

×
1377
        return nil
×
1378
}
1379

1380
// waitForCommitmentPoint waits for the commitment point to be inserted into
1381
// the local database. We'll use this method in the DLP case, to wait for the
1382
// remote party to send us their point, as we can't proceed until we have that.
1383
func (c *chainWatcher) waitForCommitmentPoint() *btcec.PublicKey {
4✔
1384
        // If we are lucky, the remote peer sent us the correct commitment
4✔
1385
        // point during channel sync, such that we can sweep our funds. If we
4✔
1386
        // cannot find the commit point, there's not much we can do other than
4✔
1387
        // wait for us to retrieve it. We will attempt to retrieve it from the
4✔
1388
        // peer each time we connect to it.
4✔
1389
        //
4✔
1390
        // TODO(halseth): actively initiate re-connection to the peer?
4✔
1391
        backoff := minCommitPointPollTimeout
4✔
1392
        for {
8✔
1393
                commitPoint, err := c.cfg.chanState.DataLossCommitPoint()
4✔
1394
                if err == nil {
8✔
1395
                        return commitPoint
4✔
1396
                }
4✔
1397

1398
                log.Errorf("Unable to retrieve commitment point for "+
×
1399
                        "channel(%v) with lost state: %v. Retrying in %v.",
×
1400
                        c.cfg.chanState.FundingOutpoint, err, backoff)
×
1401

×
1402
                select {
×
1403
                // Wait before retrying, with an exponential backoff.
1404
                case <-time.After(backoff):
×
1405
                        backoff = 2 * backoff
×
1406
                        if backoff > maxCommitPointPollTimeout {
×
1407
                                backoff = maxCommitPointPollTimeout
×
1408
                        }
×
1409

1410
                case <-c.quit:
×
1411
                        return nil
×
1412
                }
1413
        }
1414
}
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