• 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

79.06
/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/chainio"
20
        "github.com/lightningnetwork/lnd/chainntnfs"
21
        "github.com/lightningnetwork/lnd/channeldb"
22
        "github.com/lightningnetwork/lnd/fn/v2"
23
        "github.com/lightningnetwork/lnd/input"
24
        "github.com/lightningnetwork/lnd/lntypes"
25
        "github.com/lightningnetwork/lnd/lnutils"
26
        "github.com/lightningnetwork/lnd/lnwallet"
27
        "github.com/lightningnetwork/lnd/lnwire"
28
)
29

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

113
        for _, htlcs := range c.HtlcSets {
6✔
114
                if len(htlcs) != 0 {
6✔
115
                        return false
3✔
116
                }
3✔
117
        }
118

119
        return true
3✔
120
}
121

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

3✔
127
        for htlcSetKey, htlcs := range c.HtlcSets {
6✔
128
                htlcSets[htlcSetKey] = newHtlcSet(htlcs)
3✔
129
        }
3✔
130

131
        return htlcSets
3✔
132
}
133

134
// String return a human-readable representation of the CommitSet.
135
func (c *CommitSet) String() string {
3✔
136
        if c == nil {
6✔
137
                return "nil"
3✔
138
        }
3✔
139

140
        // Create a descriptive string for the ConfCommitKey.
141
        commitKey := "none"
3✔
142
        c.ConfCommitKey.WhenSome(func(k HtlcSetKey) {
6✔
143
                commitKey = k.String()
3✔
144
        })
3✔
145

146
        // Create a map to hold all the htlcs.
147
        htlcSet := make(map[string]string)
3✔
148
        for k, htlcs := range c.HtlcSets {
6✔
149
                // Create a map for this particular set.
3✔
150
                desc := make([]string, len(htlcs))
3✔
151
                for i, htlc := range htlcs {
6✔
152
                        desc[i] = fmt.Sprintf("%x", htlc.RHash)
3✔
153
                }
3✔
154

155
                // Add the description to the set key.
156
                htlcSet[k.String()] = fmt.Sprintf("count: %v, htlcs=%v",
3✔
157
                        len(htlcs), desc)
3✔
158
        }
159

160
        return fmt.Sprintf("ConfCommitKey=%v, HtlcSets=%v", commitKey, htlcSet)
3✔
161
}
162

163
// ChainEventSubscription is a struct that houses a subscription to be notified
164
// for any on-chain events related to a channel. There are three types of
165
// possible on-chain events: a cooperative channel closure, a unilateral
166
// channel closure, and a channel breach. The fourth type: a force close is
167
// locally initiated, so we don't provide any event stream for said event.
168
type ChainEventSubscription struct {
169
        // ChanPoint is that channel that chain events will be dispatched for.
170
        ChanPoint wire.OutPoint
171

172
        // RemoteUnilateralClosure is a channel that will be sent upon in the
173
        // event that the remote party's commitment transaction is confirmed.
174
        RemoteUnilateralClosure chan *RemoteUnilateralCloseInfo
175

176
        // LocalUnilateralClosure is a channel that will be sent upon in the
177
        // event that our commitment transaction is confirmed.
178
        LocalUnilateralClosure chan *LocalUnilateralCloseInfo
179

180
        // CooperativeClosure is a signal that will be sent upon once a
181
        // cooperative channel closure has been detected confirmed.
182
        CooperativeClosure chan *CooperativeCloseInfo
183

184
        // ContractBreach is a channel that will be sent upon if we detect a
185
        // contract breach. The struct sent across the channel contains all the
186
        // material required to bring the cheating channel peer to justice.
187
        ContractBreach chan *BreachCloseInfo
188

189
        // Cancel cancels the subscription to the event stream for a particular
190
        // channel. This method should be called once the caller no longer needs to
191
        // be notified of any on-chain events for a particular channel.
192
        Cancel func()
193
}
194

195
// chainWatcherConfig encapsulates all the necessary functions and interfaces
196
// needed to watch and act on on-chain events for a particular channel.
197
type chainWatcherConfig struct {
198
        // chanState is a snapshot of the persistent state of the channel that
199
        // we're watching. In the event of an on-chain event, we'll query the
200
        // database to ensure that we act using the most up to date state.
201
        chanState *channeldb.OpenChannel
202

203
        // notifier is a reference to the channel notifier that we'll use to be
204
        // notified of output spends and when transactions are confirmed.
205
        notifier chainntnfs.ChainNotifier
206

207
        // signer is the main signer instances that will be responsible for
208
        // signing any HTLC and commitment transaction generated by the state
209
        // machine.
210
        signer input.Signer
211

212
        // contractBreach is a method that will be called by the watcher if it
213
        // detects that a contract breach transaction has been confirmed. It
214
        // will only return a non-nil error when the BreachArbitrator has
215
        // preserved the necessary breach info for this channel point.
216
        contractBreach func(*lnwallet.BreachRetribution) error
217

218
        // isOurAddr is a function that returns true if the passed address is
219
        // known to us.
220
        isOurAddr func(btcutil.Address) bool
221

222
        // extractStateNumHint extracts the encoded state hint using the passed
223
        // obfuscater. This is used by the chain watcher to identify which
224
        // state was broadcast and confirmed on-chain.
225
        extractStateNumHint func(*wire.MsgTx, [lnwallet.StateHintSize]byte) uint64
226

227
        // auxLeafStore can be used to fetch information for custom channels.
228
        auxLeafStore fn.Option[lnwallet.AuxLeafStore]
229

230
        // auxResolver is used to supplement contract resolution.
231
        auxResolver fn.Option[lnwallet.AuxContractResolver]
232
}
233

234
// chainWatcher is a system that's assigned to every active channel. The duty
235
// of this system is to watch the chain for spends of the channels chan point.
236
// If a spend is detected then with chain watcher will notify all subscribers
237
// that the channel has been closed, and also give them the materials necessary
238
// to sweep the funds of the channel on chain eventually.
239
type chainWatcher struct {
240
        started int32 // To be used atomically.
241
        stopped int32 // To be used atomically.
242

243
        // Embed the blockbeat consumer struct to get access to the method
244
        // `NotifyBlockProcessed` and the `BlockbeatChan`.
245
        chainio.BeatConsumer
246

247
        quit chan struct{}
248
        wg   sync.WaitGroup
249

250
        cfg chainWatcherConfig
251

252
        // stateHintObfuscator is a 48-bit state hint that's used to obfuscate
253
        // the current state number on the commitment transactions.
254
        stateHintObfuscator [lnwallet.StateHintSize]byte
255

256
        // All the fields below are protected by this mutex.
257
        sync.Mutex
258

259
        // clientID is an ephemeral counter used to keep track of each
260
        // individual client subscription.
261
        clientID uint64
262

263
        // clientSubscriptions is a map that keeps track of all the active
264
        // client subscriptions for events related to this channel.
265
        clientSubscriptions map[uint64]*ChainEventSubscription
266

267
        // fundingSpendNtfn is the spending notification subscription for the
268
        // funding outpoint.
269
        fundingSpendNtfn *chainntnfs.SpendEvent
270

271
        // fundingConfirmedNtfn is the confirmation notification subscription
272
        // for the funding outpoint. This is only created if the channel is
273
        // both taproot and pending confirmation.
274
        //
275
        // For taproot pkscripts, `RegisterSpendNtfn` will only notify on the
276
        // outpoint being spent and not the outpoint+pkscript due to
277
        // `ComputePkScript` being unable to compute the pkscript if a key
278
        // spend is used. We need to add a `RegisterConfirmationsNtfn` here to
279
        // ensure that the outpoint+pkscript pair is confirmed before calling
280
        // `RegisterSpendNtfn`.
281
        fundingConfirmedNtfn *chainntnfs.ConfirmationEvent
282
}
283

284
// newChainWatcher returns a new instance of a chainWatcher for a channel given
285
// the chan point to watch, and also a notifier instance that will allow us to
286
// detect on chain events.
287
func newChainWatcher(cfg chainWatcherConfig) (*chainWatcher, error) {
3✔
288
        // In order to be able to detect the nature of a potential channel
3✔
289
        // closure we'll need to reconstruct the state hint bytes used to
3✔
290
        // obfuscate the commitment state number encoded in the lock time and
3✔
291
        // sequence fields.
3✔
292
        var stateHint [lnwallet.StateHintSize]byte
3✔
293
        chanState := cfg.chanState
3✔
294
        if chanState.IsInitiator {
6✔
295
                stateHint = lnwallet.DeriveStateHintObfuscator(
3✔
296
                        chanState.LocalChanCfg.PaymentBasePoint.PubKey,
3✔
297
                        chanState.RemoteChanCfg.PaymentBasePoint.PubKey,
3✔
298
                )
3✔
299
        } else {
6✔
300
                stateHint = lnwallet.DeriveStateHintObfuscator(
3✔
301
                        chanState.RemoteChanCfg.PaymentBasePoint.PubKey,
3✔
302
                        chanState.LocalChanCfg.PaymentBasePoint.PubKey,
3✔
303
                )
3✔
304
        }
3✔
305

306
        // Get the witness script for the funding output.
307
        fundingPkScript, err := deriveFundingPkScript(chanState)
3✔
308
        if err != nil {
3✔
309
                return nil, err
×
310
        }
×
311

312
        // Get the channel opening block height.
313
        heightHint := chanState.DeriveHeightHint()
3✔
314

3✔
315
        // We'll register for a notification to be dispatched if the funding
3✔
316
        // output is spent.
3✔
317
        spendNtfn, err := cfg.notifier.RegisterSpendNtfn(
3✔
318
                &chanState.FundingOutpoint, fundingPkScript, heightHint,
3✔
319
        )
3✔
320
        if err != nil {
3✔
321
                return nil, err
×
322
        }
×
323

324
        c := &chainWatcher{
3✔
325
                cfg:                 cfg,
3✔
326
                stateHintObfuscator: stateHint,
3✔
327
                quit:                make(chan struct{}),
3✔
328
                clientSubscriptions: make(map[uint64]*ChainEventSubscription),
3✔
329
                fundingSpendNtfn:    spendNtfn,
3✔
330
        }
3✔
331

3✔
332
        // If this is a pending taproot channel, we need to register for a
3✔
333
        // confirmation notification of the funding tx. Check the docs in
3✔
334
        // `fundingConfirmedNtfn` for details.
3✔
335
        if c.cfg.chanState.IsPending && c.cfg.chanState.ChanType.IsTaproot() {
6✔
336
                confNtfn, err := cfg.notifier.RegisterConfirmationsNtfn(
3✔
337
                        &chanState.FundingOutpoint.Hash, fundingPkScript, 1,
3✔
338
                        heightHint,
3✔
339
                )
3✔
340
                if err != nil {
3✔
341
                        return nil, err
×
342
                }
×
343

344
                c.fundingConfirmedNtfn = confNtfn
3✔
345
        }
346

347
        // Mount the block consumer.
348
        c.BeatConsumer = chainio.NewBeatConsumer(c.quit, c.Name())
3✔
349

3✔
350
        return c, nil
3✔
351
}
352

353
// Compile-time check for the chainio.Consumer interface.
354
var _ chainio.Consumer = (*chainWatcher)(nil)
355

356
// Name returns the name of the watcher.
357
//
358
// NOTE: part of the `chainio.Consumer` interface.
359
func (c *chainWatcher) Name() string {
3✔
360
        return fmt.Sprintf("ChainWatcher(%v)", c.cfg.chanState.FundingOutpoint)
3✔
361
}
3✔
362

363
// Start starts all goroutines that the chainWatcher needs to perform its
364
// duties.
365
func (c *chainWatcher) Start() error {
3✔
366
        if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
3✔
367
                return nil
×
368
        }
×
369

370
        log.Debugf("Starting chain watcher for ChannelPoint(%v)",
3✔
371
                c.cfg.chanState.FundingOutpoint)
3✔
372

3✔
373
        c.wg.Add(1)
3✔
374
        go c.closeObserver()
3✔
375

3✔
376
        return nil
3✔
377
}
378

379
// Stop signals the close observer to gracefully exit.
380
func (c *chainWatcher) Stop() error {
3✔
381
        if !atomic.CompareAndSwapInt32(&c.stopped, 0, 1) {
3✔
382
                return nil
×
383
        }
×
384

385
        close(c.quit)
3✔
386

3✔
387
        c.wg.Wait()
3✔
388

3✔
389
        return nil
3✔
390
}
391

392
// SubscribeChannelEvents returns an active subscription to the set of channel
393
// events for the channel watched by this chain watcher. Once clients no longer
394
// require the subscription, they should call the Cancel() method to allow the
395
// watcher to regain those committed resources.
396
func (c *chainWatcher) SubscribeChannelEvents() *ChainEventSubscription {
3✔
397

3✔
398
        c.Lock()
3✔
399
        clientID := c.clientID
3✔
400
        c.clientID++
3✔
401
        c.Unlock()
3✔
402

3✔
403
        log.Debugf("New ChainEventSubscription(id=%v) for ChannelPoint(%v)",
3✔
404
                clientID, c.cfg.chanState.FundingOutpoint)
3✔
405

3✔
406
        sub := &ChainEventSubscription{
3✔
407
                ChanPoint:               c.cfg.chanState.FundingOutpoint,
3✔
408
                RemoteUnilateralClosure: make(chan *RemoteUnilateralCloseInfo, 1),
3✔
409
                LocalUnilateralClosure:  make(chan *LocalUnilateralCloseInfo, 1),
3✔
410
                CooperativeClosure:      make(chan *CooperativeCloseInfo, 1),
3✔
411
                ContractBreach:          make(chan *BreachCloseInfo, 1),
3✔
412
                Cancel: func() {
6✔
413
                        c.Lock()
3✔
414
                        delete(c.clientSubscriptions, clientID)
3✔
415
                        c.Unlock()
3✔
416
                },
3✔
417
        }
418

419
        c.Lock()
3✔
420
        c.clientSubscriptions[clientID] = sub
3✔
421
        c.Unlock()
3✔
422

3✔
423
        return sub
3✔
424
}
425

426
// handleUnknownLocalState checks whether the passed spend _could_ be a local
427
// state that for some reason is unknown to us. This could be a state published
428
// by us before we lost state, which we will try to sweep. Or it could be one
429
// of our revoked states that somehow made it to the chain. If that's the case
430
// we cannot really hope that we'll be able to get our money back, but we'll
431
// try to sweep it anyway. If this is not an unknown local state, false is
432
// returned.
433
func (c *chainWatcher) handleUnknownLocalState(
434
        commitSpend *chainntnfs.SpendDetail, broadcastStateNum uint64,
435
        chainSet *chainSet) (bool, error) {
3✔
436

3✔
437
        // If the spend was a local commitment, at this point it must either be
3✔
438
        // a past state (we breached!) or a future state (we lost state!). In
3✔
439
        // either case, the only thing we can do is to attempt to sweep what is
3✔
440
        // there.
3✔
441

3✔
442
        // First, we'll re-derive our commitment point for this state since
3✔
443
        // this is what we use to randomize each of the keys for this state.
3✔
444
        commitSecret, err := c.cfg.chanState.RevocationProducer.AtIndex(
3✔
445
                broadcastStateNum,
3✔
446
        )
3✔
447
        if err != nil {
3✔
448
                return false, err
×
449
        }
×
450
        commitPoint := input.ComputeCommitmentPoint(commitSecret[:])
3✔
451

3✔
452
        // Now that we have the commit point, we'll derive the tweaked local
3✔
453
        // and remote keys for this state. We use our point as only we can
3✔
454
        // revoke our own commitment.
3✔
455
        commitKeyRing := lnwallet.DeriveCommitmentKeys(
3✔
456
                commitPoint, lntypes.Local, c.cfg.chanState.ChanType,
3✔
457
                &c.cfg.chanState.LocalChanCfg, &c.cfg.chanState.RemoteChanCfg,
3✔
458
        )
3✔
459

3✔
460
        auxResult, err := fn.MapOptionZ(
3✔
461
                c.cfg.auxLeafStore,
3✔
462
                //nolint:ll
3✔
463
                func(s lnwallet.AuxLeafStore) fn.Result[lnwallet.CommitDiffAuxResult] {
3✔
464
                        return s.FetchLeavesFromCommit(
×
465
                                lnwallet.NewAuxChanState(c.cfg.chanState),
×
466
                                c.cfg.chanState.LocalCommitment, *commitKeyRing,
×
467
                                lntypes.Local,
×
468
                        )
×
469
                },
×
470
        ).Unpack()
471
        if err != nil {
3✔
472
                return false, fmt.Errorf("unable to fetch aux leaves: %w", err)
×
473
        }
×
474

475
        // With the keys derived, we'll construct the remote script that'll be
476
        // present if they have a non-dust balance on the commitment.
477
        var leaseExpiry uint32
3✔
478
        if c.cfg.chanState.ChanType.HasLeaseExpiration() {
6✔
479
                leaseExpiry = c.cfg.chanState.ThawHeight
3✔
480
        }
3✔
481

482
        remoteAuxLeaf := fn.FlatMapOption(
3✔
483
                func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf {
3✔
484
                        return l.RemoteAuxLeaf
×
485
                },
×
486
        )(auxResult.AuxLeaves)
487
        remoteScript, _, err := lnwallet.CommitScriptToRemote(
3✔
488
                c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
3✔
489
                commitKeyRing.ToRemoteKey, leaseExpiry,
3✔
490
                remoteAuxLeaf,
3✔
491
        )
3✔
492
        if err != nil {
3✔
493
                return false, err
×
494
        }
×
495

496
        // Next, we'll derive our script that includes the revocation base for
497
        // the remote party allowing them to claim this output before the CSV
498
        // delay if we breach.
499
        localAuxLeaf := fn.FlatMapOption(
3✔
500
                func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf {
3✔
501
                        return l.LocalAuxLeaf
×
502
                },
×
503
        )(auxResult.AuxLeaves)
504
        localScript, err := lnwallet.CommitScriptToSelf(
3✔
505
                c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
3✔
506
                commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey,
3✔
507
                uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry,
3✔
508
                localAuxLeaf,
3✔
509
        )
3✔
510
        if err != nil {
3✔
511
                return false, err
×
512
        }
×
513

514
        // With all our scripts assembled, we'll examine the outputs of the
515
        // commitment transaction to determine if this is a local force close
516
        // or not.
517
        ourCommit := false
3✔
518
        for _, output := range commitSpend.SpendingTx.TxOut {
6✔
519
                pkScript := output.PkScript
3✔
520

3✔
521
                switch {
3✔
522
                case bytes.Equal(localScript.PkScript(), pkScript):
3✔
523
                        ourCommit = true
3✔
524

525
                case bytes.Equal(remoteScript.PkScript(), pkScript):
3✔
526
                        ourCommit = true
3✔
527
                }
528
        }
529

530
        // If the script is not present, this cannot be our commit.
531
        if !ourCommit {
6✔
532
                return false, nil
3✔
533
        }
3✔
534

535
        log.Warnf("Detected local unilateral close of unknown state %v "+
3✔
536
                "(our state=%v)", broadcastStateNum,
3✔
537
                chainSet.localCommit.CommitHeight)
3✔
538

3✔
539
        // If this is our commitment transaction, then we try to act even
3✔
540
        // though we won't be able to sweep HTLCs.
3✔
541
        chainSet.commitSet.ConfCommitKey = fn.Some(LocalHtlcSet)
3✔
542
        if err := c.dispatchLocalForceClose(
3✔
543
                commitSpend, broadcastStateNum, chainSet.commitSet,
3✔
544
        ); err != nil {
3✔
545
                return false, fmt.Errorf("unable to handle local"+
×
546
                        "close for chan_point=%v: %v",
×
547
                        c.cfg.chanState.FundingOutpoint, err)
×
548
        }
×
549

550
        return true, nil
3✔
551
}
552

553
// chainSet includes all the information we need to dispatch a channel close
554
// event to any subscribers.
555
type chainSet struct {
556
        // remoteStateNum is the commitment number of the lowest valid
557
        // commitment the remote party holds from our PoV. This value is used
558
        // to determine if the remote party is playing a state that's behind,
559
        // in line, or ahead of the latest state we know for it.
560
        remoteStateNum uint64
561

562
        // commitSet includes information pertaining to the set of active HTLCs
563
        // on each commitment.
564
        commitSet CommitSet
565

566
        // remoteCommit is the current commitment of the remote party.
567
        remoteCommit channeldb.ChannelCommitment
568

569
        // localCommit is our current commitment.
570
        localCommit channeldb.ChannelCommitment
571

572
        // remotePendingCommit points to the dangling commitment of the remote
573
        // party, if it exists. If there's no dangling commitment, then this
574
        // pointer will be nil.
575
        remotePendingCommit *channeldb.ChannelCommitment
576
}
577

578
// newChainSet creates a new chainSet given the current up to date channel
579
// state.
580
func newChainSet(chanState *channeldb.OpenChannel) (*chainSet, error) {
3✔
581
        // First, we'll grab the current unrevoked commitments for ourselves
3✔
582
        // and the remote party.
3✔
583
        localCommit, remoteCommit, err := chanState.LatestCommitments()
3✔
584
        if err != nil {
3✔
585
                return nil, fmt.Errorf("unable to fetch channel state for "+
×
586
                        "chan_point=%v: %v", chanState.FundingOutpoint, err)
×
587
        }
×
588

589
        log.Tracef("ChannelPoint(%v): local_commit_type=%v, local_commit=%v",
3✔
590
                chanState.FundingOutpoint, chanState.ChanType,
3✔
591
                spew.Sdump(localCommit))
3✔
592
        log.Tracef("ChannelPoint(%v): remote_commit_type=%v, remote_commit=%v",
3✔
593
                chanState.FundingOutpoint, chanState.ChanType,
3✔
594
                spew.Sdump(remoteCommit))
3✔
595

3✔
596
        // Fetch the current known commit height for the remote party, and
3✔
597
        // their pending commitment chain tip if it exists.
3✔
598
        remoteStateNum := remoteCommit.CommitHeight
3✔
599
        remoteChainTip, err := chanState.RemoteCommitChainTip()
3✔
600
        if err != nil && err != channeldb.ErrNoPendingCommit {
3✔
601
                return nil, fmt.Errorf("unable to obtain chain tip for "+
×
602
                        "ChannelPoint(%v): %v",
×
603
                        chanState.FundingOutpoint, err)
×
604
        }
×
605

606
        // Now that we have all the possible valid commitments, we'll make the
607
        // CommitSet the ChannelArbitrator will need in order to carry out its
608
        // duty.
609
        commitSet := CommitSet{
3✔
610
                HtlcSets: map[HtlcSetKey][]channeldb.HTLC{
3✔
611
                        LocalHtlcSet:  localCommit.Htlcs,
3✔
612
                        RemoteHtlcSet: remoteCommit.Htlcs,
3✔
613
                },
3✔
614
        }
3✔
615

3✔
616
        var remotePendingCommit *channeldb.ChannelCommitment
3✔
617
        if remoteChainTip != nil {
6✔
618
                remotePendingCommit = &remoteChainTip.Commitment
3✔
619
                log.Tracef("ChannelPoint(%v): remote_pending_commit_type=%v, "+
3✔
620
                        "remote_pending_commit=%v", chanState.FundingOutpoint,
3✔
621
                        chanState.ChanType,
3✔
622
                        spew.Sdump(remoteChainTip.Commitment))
3✔
623

3✔
624
                htlcs := remoteChainTip.Commitment.Htlcs
3✔
625
                commitSet.HtlcSets[RemotePendingHtlcSet] = htlcs
3✔
626
        }
3✔
627

628
        // We'll now retrieve the latest state of the revocation store so we
629
        // can populate the revocation information within the channel state
630
        // object that we have.
631
        //
632
        // TODO(roasbeef): mutation is bad mkay
633
        _, err = chanState.RemoteRevocationStore()
3✔
634
        if err != nil {
3✔
635
                return nil, fmt.Errorf("unable to fetch revocation state for "+
×
636
                        "chan_point=%v", chanState.FundingOutpoint)
×
637
        }
×
638

639
        return &chainSet{
3✔
640
                remoteStateNum:      remoteStateNum,
3✔
641
                commitSet:           commitSet,
3✔
642
                localCommit:         *localCommit,
3✔
643
                remoteCommit:        *remoteCommit,
3✔
644
                remotePendingCommit: remotePendingCommit,
3✔
645
        }, nil
3✔
646
}
647

648
// closeObserver is a dedicated goroutine that will watch for any closes of the
649
// channel that it's watching on chain. In the event of an on-chain event, the
650
// close observer will assembled the proper materials required to claim the
651
// funds of the channel on-chain (if required), then dispatch these as
652
// notifications to all subscribers.
653
func (c *chainWatcher) closeObserver() {
3✔
654
        defer c.wg.Done()
3✔
655
        defer c.fundingSpendNtfn.Cancel()
3✔
656

3✔
657
        log.Infof("Close observer for ChannelPoint(%v) active",
3✔
658
                c.cfg.chanState.FundingOutpoint)
3✔
659

3✔
660
        for {
6✔
661
                select {
3✔
662
                // A new block is received, we will check whether this block
663
                // contains a spending tx that we are interested in.
664
                case beat := <-c.BlockbeatChan:
3✔
665
                        log.Debugf("ChainWatcher(%v) received blockbeat %v",
3✔
666
                                c.cfg.chanState.FundingOutpoint, beat.Height())
3✔
667

3✔
668
                        // Process the block.
3✔
669
                        c.handleBlockbeat(beat)
3✔
670

671
                // If the funding outpoint is spent, we now go ahead and handle
672
                // it. Note that we cannot rely solely on the `block` event
673
                // above to trigger a close event, as deep down, the receiving
674
                // of block notifications and the receiving of spending
675
                // notifications are done in two different goroutines, so the
676
                // expected order: [receive block -> receive spend] is not
677
                // guaranteed .
678
                case spend, ok := <-c.fundingSpendNtfn.Spend:
3✔
679
                        // If the channel was closed, then this means that the
3✔
680
                        // notifier exited, so we will as well.
3✔
681
                        if !ok {
6✔
682
                                return
3✔
683
                        }
3✔
684

685
                        err := c.handleCommitSpend(spend)
3✔
686
                        if err != nil {
3✔
687
                                log.Errorf("Failed to handle commit spend: %v",
×
688
                                        err)
×
689
                        }
×
690

691
                // The chainWatcher has been signalled to exit, so we'll do so
692
                // now.
693
                case <-c.quit:
3✔
694
                        return
3✔
695
                }
696
        }
697
}
698

699
// handleKnownLocalState checks whether the passed spend is a local state that
700
// is known to us (the current state). If so we will act on this state using
701
// the passed chainSet. If this is not a known local state, false is returned.
702
func (c *chainWatcher) handleKnownLocalState(
703
        commitSpend *chainntnfs.SpendDetail, broadcastStateNum uint64,
704
        chainSet *chainSet) (bool, error) {
3✔
705

3✔
706
        // If the channel is recovered, we won't have a local commit to check
3✔
707
        // against, so immediately return.
3✔
708
        if c.cfg.chanState.HasChanStatus(channeldb.ChanStatusRestored) {
6✔
709
                return false, nil
3✔
710
        }
3✔
711

712
        commitTxBroadcast := commitSpend.SpendingTx
3✔
713
        commitHash := commitTxBroadcast.TxHash()
3✔
714

3✔
715
        // Check whether our latest local state hit the chain.
3✔
716
        if chainSet.localCommit.CommitTx.TxHash() != commitHash {
6✔
717
                return false, nil
3✔
718
        }
3✔
719

720
        chainSet.commitSet.ConfCommitKey = fn.Some(LocalHtlcSet)
3✔
721
        if err := c.dispatchLocalForceClose(
3✔
722
                commitSpend, broadcastStateNum, chainSet.commitSet,
3✔
723
        ); err != nil {
3✔
724
                return false, fmt.Errorf("unable to handle local"+
×
725
                        "close for chan_point=%v: %v",
×
726
                        c.cfg.chanState.FundingOutpoint, err)
×
727
        }
×
728

729
        return true, nil
3✔
730
}
731

732
// handleKnownRemoteState checks whether the passed spend is a remote state
733
// that is known to us (a revoked, current or pending state). If so we will act
734
// on this state using the passed chainSet. If this is not a known remote
735
// state, false is returned.
736
func (c *chainWatcher) handleKnownRemoteState(
737
        commitSpend *chainntnfs.SpendDetail, broadcastStateNum uint64,
738
        chainSet *chainSet) (bool, error) {
3✔
739

3✔
740
        // If the channel is recovered, we won't have any remote commit to
3✔
741
        // check against, so imemdiately return.
3✔
742
        if c.cfg.chanState.HasChanStatus(channeldb.ChanStatusRestored) {
6✔
743
                return false, nil
3✔
744
        }
3✔
745

746
        commitTxBroadcast := commitSpend.SpendingTx
3✔
747
        commitHash := commitTxBroadcast.TxHash()
3✔
748

3✔
749
        switch {
3✔
750
        // If the spending transaction matches the current latest state, then
751
        // they've initiated a unilateral close. So we'll trigger the
752
        // unilateral close signal so subscribers can clean up the state as
753
        // necessary.
754
        case chainSet.remoteCommit.CommitTx.TxHash() == commitHash:
3✔
755
                log.Infof("Remote party broadcast base set, "+
3✔
756
                        "commit_num=%v", chainSet.remoteStateNum)
3✔
757

3✔
758
                chainSet.commitSet.ConfCommitKey = fn.Some(RemoteHtlcSet)
3✔
759
                err := c.dispatchRemoteForceClose(
3✔
760
                        commitSpend, chainSet.remoteCommit,
3✔
761
                        chainSet.commitSet,
3✔
762
                        c.cfg.chanState.RemoteCurrentRevocation,
3✔
763
                )
3✔
764
                if err != nil {
3✔
765
                        return false, fmt.Errorf("unable to handle remote "+
×
766
                                "close for chan_point=%v: %v",
×
767
                                c.cfg.chanState.FundingOutpoint, err)
×
768
                }
×
769

770
                return true, nil
3✔
771

772
        // We'll also handle the case of the remote party broadcasting
773
        // their commitment transaction which is one height above ours.
774
        // This case can arise when we initiate a state transition, but
775
        // the remote party has a fail crash _after_ accepting the new
776
        // state, but _before_ sending their signature to us.
777
        case chainSet.remotePendingCommit != nil &&
UNCOV
778
                chainSet.remotePendingCommit.CommitTx.TxHash() == commitHash:
×
UNCOV
779

×
UNCOV
780
                log.Infof("Remote party broadcast pending set, "+
×
UNCOV
781
                        "commit_num=%v", chainSet.remoteStateNum+1)
×
UNCOV
782

×
UNCOV
783
                chainSet.commitSet.ConfCommitKey = fn.Some(RemotePendingHtlcSet)
×
UNCOV
784
                err := c.dispatchRemoteForceClose(
×
UNCOV
785
                        commitSpend, *chainSet.remotePendingCommit,
×
UNCOV
786
                        chainSet.commitSet,
×
UNCOV
787
                        c.cfg.chanState.RemoteNextRevocation,
×
UNCOV
788
                )
×
UNCOV
789
                if err != nil {
×
790
                        return false, fmt.Errorf("unable to handle remote "+
×
791
                                "close for chan_point=%v: %v",
×
792
                                c.cfg.chanState.FundingOutpoint, err)
×
793
                }
×
794

UNCOV
795
                return true, nil
×
796
        }
797

798
        // This is neither a remote force close or a "future" commitment, we
799
        // now check whether it's a remote breach and properly handle it.
800
        return c.handlePossibleBreach(commitSpend, broadcastStateNum, chainSet)
3✔
801
}
802

803
// handlePossibleBreach checks whether the remote has breached and dispatches a
804
// breach resolution to claim funds.
805
func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail,
806
        broadcastStateNum uint64, chainSet *chainSet) (bool, error) {
3✔
807

3✔
808
        // We check if we have a revoked state at this state num that matches
3✔
809
        // the spend transaction.
3✔
810
        spendHeight := uint32(commitSpend.SpendingHeight)
3✔
811
        retribution, err := lnwallet.NewBreachRetribution(
3✔
812
                c.cfg.chanState, broadcastStateNum, spendHeight,
3✔
813
                commitSpend.SpendingTx, c.cfg.auxLeafStore, c.cfg.auxResolver,
3✔
814
        )
3✔
815

3✔
816
        switch {
3✔
817
        // If we had no log entry at this height, this was not a revoked state.
818
        case err == channeldb.ErrLogEntryNotFound:
3✔
819
                return false, nil
3✔
820
        case err == channeldb.ErrNoPastDeltas:
3✔
821
                return false, nil
3✔
822

823
        case err != nil:
×
824
                return false, fmt.Errorf("unable to create breach "+
×
825
                        "retribution: %v", err)
×
826
        }
827

828
        // We found a revoked state at this height, but it could still be our
829
        // own broadcasted state we are looking at. Therefore check that the
830
        // commit matches before assuming it was a breach.
831
        commitHash := commitSpend.SpendingTx.TxHash()
3✔
832
        if retribution.BreachTxHash != commitHash {
3✔
833
                return false, nil
×
834
        }
×
835

836
        // Create an AnchorResolution for the breached state.
837
        anchorRes, err := lnwallet.NewAnchorResolution(
3✔
838
                c.cfg.chanState, commitSpend.SpendingTx, retribution.KeyRing,
3✔
839
                lntypes.Remote,
3✔
840
        )
3✔
841
        if err != nil {
3✔
842
                return false, fmt.Errorf("unable to create anchor "+
×
843
                        "resolution: %v", err)
×
844
        }
×
845

846
        // We'll set the ConfCommitKey here as the remote htlc set. This is
847
        // only used to ensure a nil-pointer-dereference doesn't occur and is
848
        // not used otherwise. The HTLC's may not exist for the
849
        // RemotePendingHtlcSet.
850
        chainSet.commitSet.ConfCommitKey = fn.Some(RemoteHtlcSet)
3✔
851

3✔
852
        // THEY'RE ATTEMPTING TO VIOLATE THE CONTRACT LAID OUT WITHIN THE
3✔
853
        // PAYMENT CHANNEL. Therefore we close the signal indicating a revoked
3✔
854
        // broadcast to allow subscribers to swiftly dispatch justice!!!
3✔
855
        err = c.dispatchContractBreach(
3✔
856
                commitSpend, chainSet, broadcastStateNum, retribution,
3✔
857
                anchorRes,
3✔
858
        )
3✔
859
        if err != nil {
3✔
860
                return false, fmt.Errorf("unable to handle channel "+
×
861
                        "breach for chan_point=%v: %v",
×
862
                        c.cfg.chanState.FundingOutpoint, err)
×
863
        }
×
864

865
        return true, nil
3✔
866
}
867

868
// handleUnknownRemoteState is the last attempt we make at reclaiming funds
869
// from the closed channel, by checkin whether the passed spend _could_ be a
870
// remote spend that is unknown to us (we lost state). We will try to initiate
871
// Data Loss Protection in order to restore our commit point and reclaim our
872
// funds from the channel. If we are not able to act on it, false is returned.
873
func (c *chainWatcher) handleUnknownRemoteState(
874
        commitSpend *chainntnfs.SpendDetail, broadcastStateNum uint64,
875
        chainSet *chainSet) (bool, error) {
3✔
876

3✔
877
        log.Warnf("Remote node broadcast state #%v, "+
3✔
878
                "which is more than 1 beyond best known "+
3✔
879
                "state #%v!!! Attempting recovery...",
3✔
880
                broadcastStateNum, chainSet.remoteStateNum)
3✔
881

3✔
882
        // If this isn't a tweakless commitment, then we'll need to wait for
3✔
883
        // the remote party's latest unrevoked commitment point to be presented
3✔
884
        // to us as we need this to sweep. Otherwise, we can dispatch the
3✔
885
        // remote close and sweep immediately using a fake commitPoint as it
3✔
886
        // isn't actually needed for recovery anymore.
3✔
887
        commitPoint := c.cfg.chanState.RemoteCurrentRevocation
3✔
888
        tweaklessCommit := c.cfg.chanState.ChanType.IsTweakless()
3✔
889
        if !tweaklessCommit {
3✔
UNCOV
890
                commitPoint = c.waitForCommitmentPoint()
×
UNCOV
891
                if commitPoint == nil {
×
892
                        return false, fmt.Errorf("unable to get commit point")
×
893
                }
×
894

UNCOV
895
                log.Infof("Recovered commit point(%x) for "+
×
UNCOV
896
                        "channel(%v)! Now attempting to use it to "+
×
UNCOV
897
                        "sweep our funds...",
×
UNCOV
898
                        commitPoint.SerializeCompressed(),
×
UNCOV
899
                        c.cfg.chanState.FundingOutpoint)
×
900
        } else {
3✔
901
                log.Infof("ChannelPoint(%v) is tweakless, "+
3✔
902
                        "moving to sweep directly on chain",
3✔
903
                        c.cfg.chanState.FundingOutpoint)
3✔
904
        }
3✔
905

906
        // Since we don't have the commitment stored for this state, we'll just
907
        // pass an empty commitment within the commitment set. Note that this
908
        // means we won't be able to recover any HTLC funds.
909
        //
910
        // TODO(halseth): can we try to recover some HTLCs?
911
        chainSet.commitSet.ConfCommitKey = fn.Some(RemoteHtlcSet)
3✔
912
        err := c.dispatchRemoteForceClose(
3✔
913
                commitSpend, channeldb.ChannelCommitment{},
3✔
914
                chainSet.commitSet, commitPoint,
3✔
915
        )
3✔
916
        if err != nil {
3✔
917
                return false, fmt.Errorf("unable to handle remote "+
×
918
                        "close for chan_point=%v: %v",
×
919
                        c.cfg.chanState.FundingOutpoint, err)
×
920
        }
×
921

922
        return true, nil
3✔
923
}
924

925
// toSelfAmount takes a transaction and returns the sum of all outputs that pay
926
// to a script that the wallet controls or the channel defines as its delivery
927
// script . If no outputs pay to us (determined by these criteria), then we
928
// return zero. This is possible as our output may have been trimmed due to
929
// being dust.
930
func (c *chainWatcher) toSelfAmount(tx *wire.MsgTx) btcutil.Amount {
3✔
931
        // There are two main cases we have to handle here. First, in the coop
3✔
932
        // close case we will always have saved the delivery address we used
3✔
933
        // whether it was from the upfront shutdown, from the delivery address
3✔
934
        // requested at close time, or even an automatically generated one. All
3✔
935
        // coop-close cases can be identified in the following manner:
3✔
936
        shutdown, _ := c.cfg.chanState.ShutdownInfo()
3✔
937
        oDeliveryAddr := fn.MapOption(
3✔
938
                func(i channeldb.ShutdownInfo) lnwire.DeliveryAddress {
6✔
939
                        return i.DeliveryScript.Val
3✔
940
                })(shutdown)
3✔
941

942
        // Here we define a function capable of identifying whether an output
943
        // corresponds with our local delivery script from a ShutdownInfo if we
944
        // have a ShutdownInfo for this chainWatcher's underlying channel.
945
        //
946
        // isDeliveryOutput :: *TxOut -> bool
947
        isDeliveryOutput := func(o *wire.TxOut) bool {
6✔
948
                return fn.ElimOption(
3✔
949
                        oDeliveryAddr,
3✔
950
                        // If we don't have a delivery addr, then the output
3✔
951
                        // can't match it.
3✔
952
                        func() bool { return false },
3✔
953
                        // Otherwise if the PkScript of the TxOut matches our
954
                        // delivery script then this is a delivery output.
955
                        func(a lnwire.DeliveryAddress) bool {
3✔
956
                                return slices.Equal(a, o.PkScript)
3✔
957
                        },
3✔
958
                )
959
        }
960

961
        // Here we define a function capable of identifying whether an output
962
        // belongs to the LND wallet. We use this as a heuristic in the case
963
        // where we might be looking for spendable force closure outputs.
964
        //
965
        // isWalletOutput :: *TxOut -> bool
966
        isWalletOutput := func(out *wire.TxOut) bool {
6✔
967
                _, addrs, _, err := txscript.ExtractPkScriptAddrs(
3✔
968
                        // Doesn't matter what net we actually pass in.
3✔
969
                        out.PkScript, &chaincfg.TestNet3Params,
3✔
970
                )
3✔
971
                if err != nil {
3✔
972
                        return false
×
973
                }
×
974

975
                return fn.Any(addrs, c.cfg.isOurAddr)
3✔
976
        }
977

978
        // Grab all of the outputs that correspond with our delivery address
979
        // or our wallet is aware of.
980
        outs := fn.Filter(tx.TxOut, fn.PredOr(isDeliveryOutput, isWalletOutput))
3✔
981

3✔
982
        // Grab the values for those outputs.
3✔
983
        vals := fn.Map(outs, func(o *wire.TxOut) int64 { return o.Value })
6✔
984

985
        // Return the sum.
986
        return btcutil.Amount(fn.Sum(vals))
3✔
987
}
988

989
// dispatchCooperativeClose processed a detect cooperative channel closure.
990
// We'll use the spending transaction to locate our output within the
991
// transaction, then clean up the database state. We'll also dispatch a
992
// notification to all subscribers that the channel has been closed in this
993
// manner.
994
func (c *chainWatcher) dispatchCooperativeClose(commitSpend *chainntnfs.SpendDetail) error {
3✔
995
        broadcastTx := commitSpend.SpendingTx
3✔
996

3✔
997
        log.Infof("Cooperative closure for ChannelPoint(%v): %v",
3✔
998
                c.cfg.chanState.FundingOutpoint, spew.Sdump(broadcastTx))
3✔
999

3✔
1000
        // If the input *is* final, then we'll check to see which output is
3✔
1001
        // ours.
3✔
1002
        localAmt := c.toSelfAmount(broadcastTx)
3✔
1003

3✔
1004
        // Once this is known, we'll mark the state as fully closed in the
3✔
1005
        // database. We can do this as a cooperatively closed channel has all
3✔
1006
        // its outputs resolved after only one confirmation.
3✔
1007
        closeSummary := &channeldb.ChannelCloseSummary{
3✔
1008
                ChanPoint:               c.cfg.chanState.FundingOutpoint,
3✔
1009
                ChainHash:               c.cfg.chanState.ChainHash,
3✔
1010
                ClosingTXID:             *commitSpend.SpenderTxHash,
3✔
1011
                RemotePub:               c.cfg.chanState.IdentityPub,
3✔
1012
                Capacity:                c.cfg.chanState.Capacity,
3✔
1013
                CloseHeight:             uint32(commitSpend.SpendingHeight),
3✔
1014
                SettledBalance:          localAmt,
3✔
1015
                CloseType:               channeldb.CooperativeClose,
3✔
1016
                ShortChanID:             c.cfg.chanState.ShortChanID(),
3✔
1017
                IsPending:               true,
3✔
1018
                RemoteCurrentRevocation: c.cfg.chanState.RemoteCurrentRevocation,
3✔
1019
                RemoteNextRevocation:    c.cfg.chanState.RemoteNextRevocation,
3✔
1020
                LocalChanConfig:         c.cfg.chanState.LocalChanCfg,
3✔
1021
        }
3✔
1022

3✔
1023
        // Attempt to add a channel sync message to the close summary.
3✔
1024
        chanSync, err := c.cfg.chanState.ChanSyncMsg()
3✔
1025
        if err != nil {
3✔
1026
                log.Errorf("ChannelPoint(%v): unable to create channel sync "+
×
1027
                        "message: %v", c.cfg.chanState.FundingOutpoint, err)
×
1028
        } else {
3✔
1029
                closeSummary.LastChanSyncMsg = chanSync
3✔
1030
        }
3✔
1031

1032
        // Create a summary of all the information needed to handle the
1033
        // cooperative closure.
1034
        closeInfo := &CooperativeCloseInfo{
3✔
1035
                ChannelCloseSummary: closeSummary,
3✔
1036
        }
3✔
1037

3✔
1038
        // With the event processed, we'll now notify all subscribers of the
3✔
1039
        // event.
3✔
1040
        c.Lock()
3✔
1041
        for _, sub := range c.clientSubscriptions {
6✔
1042
                select {
3✔
1043
                case sub.CooperativeClosure <- closeInfo:
3✔
1044
                case <-c.quit:
×
1045
                        c.Unlock()
×
1046
                        return fmt.Errorf("exiting")
×
1047
                }
1048
        }
1049
        c.Unlock()
3✔
1050

3✔
1051
        return nil
3✔
1052
}
1053

1054
// dispatchLocalForceClose processes a unilateral close by us being confirmed.
1055
func (c *chainWatcher) dispatchLocalForceClose(
1056
        commitSpend *chainntnfs.SpendDetail,
1057
        stateNum uint64, commitSet CommitSet) error {
3✔
1058

3✔
1059
        log.Infof("Local unilateral close of ChannelPoint(%v) "+
3✔
1060
                "detected", c.cfg.chanState.FundingOutpoint)
3✔
1061

3✔
1062
        forceClose, err := lnwallet.NewLocalForceCloseSummary(
3✔
1063
                c.cfg.chanState, c.cfg.signer, commitSpend.SpendingTx, stateNum,
3✔
1064
                c.cfg.auxLeafStore, c.cfg.auxResolver,
3✔
1065
        )
3✔
1066
        if err != nil {
3✔
1067
                return err
×
1068
        }
×
1069

1070
        // As we've detected that the channel has been closed, immediately
1071
        // creating a close summary for future usage by related sub-systems.
1072
        chanSnapshot := forceClose.ChanSnapshot
3✔
1073
        closeSummary := &channeldb.ChannelCloseSummary{
3✔
1074
                ChanPoint:               chanSnapshot.ChannelPoint,
3✔
1075
                ChainHash:               chanSnapshot.ChainHash,
3✔
1076
                ClosingTXID:             forceClose.CloseTx.TxHash(),
3✔
1077
                RemotePub:               &chanSnapshot.RemoteIdentity,
3✔
1078
                Capacity:                chanSnapshot.Capacity,
3✔
1079
                CloseType:               channeldb.LocalForceClose,
3✔
1080
                IsPending:               true,
3✔
1081
                ShortChanID:             c.cfg.chanState.ShortChanID(),
3✔
1082
                CloseHeight:             uint32(commitSpend.SpendingHeight),
3✔
1083
                RemoteCurrentRevocation: c.cfg.chanState.RemoteCurrentRevocation,
3✔
1084
                RemoteNextRevocation:    c.cfg.chanState.RemoteNextRevocation,
3✔
1085
                LocalChanConfig:         c.cfg.chanState.LocalChanCfg,
3✔
1086
        }
3✔
1087

3✔
1088
        resolutions, err := forceClose.ContractResolutions.UnwrapOrErr(
3✔
1089
                fmt.Errorf("resolutions not found"),
3✔
1090
        )
3✔
1091
        if err != nil {
3✔
1092
                return err
×
1093
        }
×
1094

1095
        // If our commitment output isn't dust or we have active HTLC's on the
1096
        // commitment transaction, then we'll populate the balances on the
1097
        // close channel summary.
1098
        if resolutions.CommitResolution != nil {
6✔
1099
                localBalance := chanSnapshot.LocalBalance.ToSatoshis()
3✔
1100
                closeSummary.SettledBalance = localBalance
3✔
1101
                closeSummary.TimeLockedBalance = localBalance
3✔
1102
        }
3✔
1103

1104
        if resolutions.HtlcResolutions != nil {
6✔
1105
                for _, htlc := range resolutions.HtlcResolutions.OutgoingHTLCs {
6✔
1106
                        htlcValue := btcutil.Amount(
3✔
1107
                                htlc.SweepSignDesc.Output.Value,
3✔
1108
                        )
3✔
1109
                        closeSummary.TimeLockedBalance += htlcValue
3✔
1110
                }
3✔
1111
        }
1112

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

1122
        // With the event processed, we'll now notify all subscribers of the
1123
        // event.
1124
        closeInfo := &LocalUnilateralCloseInfo{
3✔
1125
                SpendDetail:            commitSpend,
3✔
1126
                LocalForceCloseSummary: forceClose,
3✔
1127
                ChannelCloseSummary:    closeSummary,
3✔
1128
                CommitSet:              commitSet,
3✔
1129
        }
3✔
1130
        c.Lock()
3✔
1131
        for _, sub := range c.clientSubscriptions {
6✔
1132
                select {
3✔
1133
                case sub.LocalUnilateralClosure <- closeInfo:
3✔
1134
                case <-c.quit:
×
1135
                        c.Unlock()
×
1136
                        return fmt.Errorf("exiting")
×
1137
                }
1138
        }
1139
        c.Unlock()
3✔
1140

3✔
1141
        return nil
3✔
1142
}
1143

1144
// dispatchRemoteForceClose processes a detected unilateral channel closure by
1145
// the remote party. This function will prepare a UnilateralCloseSummary which
1146
// will then be sent to any subscribers allowing them to resolve all our funds
1147
// in the channel on chain. Once this close summary is prepared, all registered
1148
// subscribers will receive a notification of this event. The commitPoint
1149
// argument should be set to the per_commitment_point corresponding to the
1150
// spending commitment.
1151
//
1152
// NOTE: The remoteCommit argument should be set to the stored commitment for
1153
// this particular state. If we don't have the commitment stored (should only
1154
// happen in case we have lost state) it should be set to an empty struct, in
1155
// which case we will attempt to sweep the non-HTLC output using the passed
1156
// commitPoint.
1157
func (c *chainWatcher) dispatchRemoteForceClose(
1158
        commitSpend *chainntnfs.SpendDetail,
1159
        remoteCommit channeldb.ChannelCommitment,
1160
        commitSet CommitSet, commitPoint *btcec.PublicKey) error {
3✔
1161

3✔
1162
        log.Infof("Unilateral close of ChannelPoint(%v) "+
3✔
1163
                "detected", c.cfg.chanState.FundingOutpoint)
3✔
1164

3✔
1165
        // First, we'll create a closure summary that contains all the
3✔
1166
        // materials required to let each subscriber sweep the funds in the
3✔
1167
        // channel on-chain.
3✔
1168
        uniClose, err := lnwallet.NewUnilateralCloseSummary(
3✔
1169
                c.cfg.chanState, c.cfg.signer, commitSpend, remoteCommit,
3✔
1170
                commitPoint, c.cfg.auxLeafStore, c.cfg.auxResolver,
3✔
1171
        )
3✔
1172
        if err != nil {
3✔
1173
                return err
×
1174
        }
×
1175

1176
        // With the event processed, we'll now notify all subscribers of the
1177
        // event.
1178
        c.Lock()
3✔
1179
        for _, sub := range c.clientSubscriptions {
6✔
1180
                select {
3✔
1181
                case sub.RemoteUnilateralClosure <- &RemoteUnilateralCloseInfo{
1182
                        UnilateralCloseSummary: uniClose,
1183
                        CommitSet:              commitSet,
1184
                }:
3✔
1185
                case <-c.quit:
×
1186
                        c.Unlock()
×
1187
                        return fmt.Errorf("exiting")
×
1188
                }
1189
        }
1190
        c.Unlock()
3✔
1191

3✔
1192
        return nil
3✔
1193
}
1194

1195
// dispatchContractBreach processes a detected contract breached by the remote
1196
// party. This method is to be called once we detect that the remote party has
1197
// broadcast a prior revoked commitment state. This method well prepare all the
1198
// materials required to bring the cheater to justice, then notify all
1199
// registered subscribers of this event.
1200
func (c *chainWatcher) dispatchContractBreach(spendEvent *chainntnfs.SpendDetail,
1201
        chainSet *chainSet, broadcastStateNum uint64,
1202
        retribution *lnwallet.BreachRetribution,
1203
        anchorRes *lnwallet.AnchorResolution) error {
3✔
1204

3✔
1205
        log.Warnf("Remote peer has breached the channel contract for "+
3✔
1206
                "ChannelPoint(%v). Revoked state #%v was broadcast!!!",
3✔
1207
                c.cfg.chanState.FundingOutpoint, broadcastStateNum)
3✔
1208

3✔
1209
        if err := c.cfg.chanState.MarkBorked(); err != nil {
3✔
1210
                return fmt.Errorf("unable to mark channel as borked: %w", err)
×
1211
        }
×
1212

1213
        spendHeight := uint32(spendEvent.SpendingHeight)
3✔
1214

3✔
1215
        log.Debugf("Punishment breach retribution created: %v",
3✔
1216
                lnutils.NewLogClosure(func() string {
6✔
1217
                        retribution.KeyRing.LocalHtlcKey = nil
3✔
1218
                        retribution.KeyRing.RemoteHtlcKey = nil
3✔
1219
                        retribution.KeyRing.ToLocalKey = nil
3✔
1220
                        retribution.KeyRing.ToRemoteKey = nil
3✔
1221
                        retribution.KeyRing.RevocationKey = nil
3✔
1222
                        return spew.Sdump(retribution)
3✔
1223
                }))
3✔
1224

1225
        settledBalance := chainSet.remoteCommit.LocalBalance.ToSatoshis()
3✔
1226
        closeSummary := channeldb.ChannelCloseSummary{
3✔
1227
                ChanPoint:               c.cfg.chanState.FundingOutpoint,
3✔
1228
                ChainHash:               c.cfg.chanState.ChainHash,
3✔
1229
                ClosingTXID:             *spendEvent.SpenderTxHash,
3✔
1230
                CloseHeight:             spendHeight,
3✔
1231
                RemotePub:               c.cfg.chanState.IdentityPub,
3✔
1232
                Capacity:                c.cfg.chanState.Capacity,
3✔
1233
                SettledBalance:          settledBalance,
3✔
1234
                CloseType:               channeldb.BreachClose,
3✔
1235
                IsPending:               true,
3✔
1236
                ShortChanID:             c.cfg.chanState.ShortChanID(),
3✔
1237
                RemoteCurrentRevocation: c.cfg.chanState.RemoteCurrentRevocation,
3✔
1238
                RemoteNextRevocation:    c.cfg.chanState.RemoteNextRevocation,
3✔
1239
                LocalChanConfig:         c.cfg.chanState.LocalChanCfg,
3✔
1240
        }
3✔
1241

3✔
1242
        // Attempt to add a channel sync message to the close summary.
3✔
1243
        chanSync, err := c.cfg.chanState.ChanSyncMsg()
3✔
1244
        if err != nil {
3✔
1245
                log.Errorf("ChannelPoint(%v): unable to create channel sync "+
×
1246
                        "message: %v", c.cfg.chanState.FundingOutpoint, err)
×
1247
        } else {
3✔
1248
                closeSummary.LastChanSyncMsg = chanSync
3✔
1249
        }
3✔
1250

1251
        // Hand the retribution info over to the BreachArbitrator. This function
1252
        // will wait for a response from the breach arbiter and then proceed to
1253
        // send a BreachCloseInfo to the channel arbitrator. The channel arb
1254
        // will then mark the channel as closed after resolutions and the
1255
        // commit set are logged in the arbitrator log.
1256
        if err := c.cfg.contractBreach(retribution); err != nil {
3✔
1257
                log.Errorf("unable to hand breached contract off to "+
×
1258
                        "BreachArbitrator: %v", err)
×
1259
                return err
×
1260
        }
×
1261

1262
        breachRes := &BreachResolution{
3✔
1263
                FundingOutPoint: c.cfg.chanState.FundingOutpoint,
3✔
1264
        }
3✔
1265

3✔
1266
        breachInfo := &BreachCloseInfo{
3✔
1267
                CommitHash:       spendEvent.SpendingTx.TxHash(),
3✔
1268
                BreachResolution: breachRes,
3✔
1269
                AnchorResolution: anchorRes,
3✔
1270
                CommitSet:        chainSet.commitSet,
3✔
1271
                CloseSummary:     closeSummary,
3✔
1272
        }
3✔
1273

3✔
1274
        // With the event processed and channel closed, we'll now notify all
3✔
1275
        // subscribers of the event.
3✔
1276
        c.Lock()
3✔
1277
        for _, sub := range c.clientSubscriptions {
6✔
1278
                select {
3✔
1279
                case sub.ContractBreach <- breachInfo:
3✔
1280
                case <-c.quit:
×
1281
                        c.Unlock()
×
1282
                        return fmt.Errorf("quitting")
×
1283
                }
1284
        }
1285
        c.Unlock()
3✔
1286

3✔
1287
        return nil
3✔
1288
}
1289

1290
// waitForCommitmentPoint waits for the commitment point to be inserted into
1291
// the local database. We'll use this method in the DLP case, to wait for the
1292
// remote party to send us their point, as we can't proceed until we have that.
UNCOV
1293
func (c *chainWatcher) waitForCommitmentPoint() *btcec.PublicKey {
×
UNCOV
1294
        // If we are lucky, the remote peer sent us the correct commitment
×
UNCOV
1295
        // point during channel sync, such that we can sweep our funds. If we
×
UNCOV
1296
        // cannot find the commit point, there's not much we can do other than
×
UNCOV
1297
        // wait for us to retrieve it. We will attempt to retrieve it from the
×
UNCOV
1298
        // peer each time we connect to it.
×
UNCOV
1299
        //
×
UNCOV
1300
        // TODO(halseth): actively initiate re-connection to the peer?
×
UNCOV
1301
        backoff := minCommitPointPollTimeout
×
UNCOV
1302
        for {
×
UNCOV
1303
                commitPoint, err := c.cfg.chanState.DataLossCommitPoint()
×
UNCOV
1304
                if err == nil {
×
UNCOV
1305
                        return commitPoint
×
UNCOV
1306
                }
×
1307

1308
                log.Errorf("Unable to retrieve commitment point for "+
×
1309
                        "channel(%v) with lost state: %v. Retrying in %v.",
×
1310
                        c.cfg.chanState.FundingOutpoint, err, backoff)
×
1311

×
1312
                select {
×
1313
                // Wait before retrying, with an exponential backoff.
1314
                case <-time.After(backoff):
×
1315
                        backoff = 2 * backoff
×
1316
                        if backoff > maxCommitPointPollTimeout {
×
1317
                                backoff = maxCommitPointPollTimeout
×
1318
                        }
×
1319

1320
                case <-c.quit:
×
1321
                        return nil
×
1322
                }
1323
        }
1324
}
1325

1326
// deriveFundingPkScript derives the script used in the funding output.
1327
func deriveFundingPkScript(chanState *channeldb.OpenChannel) ([]byte, error) {
3✔
1328
        localKey := chanState.LocalChanCfg.MultiSigKey.PubKey
3✔
1329
        remoteKey := chanState.RemoteChanCfg.MultiSigKey.PubKey
3✔
1330

3✔
1331
        var (
3✔
1332
                err             error
3✔
1333
                fundingPkScript []byte
3✔
1334
        )
3✔
1335

3✔
1336
        if chanState.ChanType.IsTaproot() {
6✔
1337
                fundingPkScript, _, err = input.GenTaprootFundingScript(
3✔
1338
                        localKey, remoteKey, 0, chanState.TapscriptRoot,
3✔
1339
                )
3✔
1340
                if err != nil {
3✔
1341
                        return nil, err
×
1342
                }
×
1343
        } else {
3✔
1344
                multiSigScript, err := input.GenMultiSigScript(
3✔
1345
                        localKey.SerializeCompressed(),
3✔
1346
                        remoteKey.SerializeCompressed(),
3✔
1347
                )
3✔
1348
                if err != nil {
3✔
1349
                        return nil, err
×
1350
                }
×
1351
                fundingPkScript, err = input.WitnessScriptHash(multiSigScript)
3✔
1352
                if err != nil {
3✔
1353
                        return nil, err
×
1354
                }
×
1355
        }
1356

1357
        return fundingPkScript, nil
3✔
1358
}
1359

1360
// handleCommitSpend takes a spending tx of the funding output and handles the
1361
// channel close based on the closure type.
1362
func (c *chainWatcher) handleCommitSpend(
1363
        commitSpend *chainntnfs.SpendDetail) error {
3✔
1364

3✔
1365
        commitTxBroadcast := commitSpend.SpendingTx
3✔
1366

3✔
1367
        // First, we'll construct the chainset which includes all the data we
3✔
1368
        // need to dispatch an event to our subscribers about this possible
3✔
1369
        // channel close event.
3✔
1370
        chainSet, err := newChainSet(c.cfg.chanState)
3✔
1371
        if err != nil {
3✔
1372
                return fmt.Errorf("create commit set: %w", err)
×
1373
        }
×
1374

1375
        // Decode the state hint encoded within the commitment transaction to
1376
        // determine if this is a revoked state or not.
1377
        obfuscator := c.stateHintObfuscator
3✔
1378
        broadcastStateNum := c.cfg.extractStateNumHint(
3✔
1379
                commitTxBroadcast, obfuscator,
3✔
1380
        )
3✔
1381

3✔
1382
        // We'll go on to check whether it could be our own commitment that was
3✔
1383
        // published and know is confirmed.
3✔
1384
        ok, err := c.handleKnownLocalState(
3✔
1385
                commitSpend, broadcastStateNum, chainSet,
3✔
1386
        )
3✔
1387
        if err != nil {
3✔
1388
                return fmt.Errorf("handle known local state: %w", err)
×
1389
        }
×
1390
        if ok {
6✔
1391
                return nil
3✔
1392
        }
3✔
1393

1394
        // Now that we know it is neither a non-cooperative closure nor a local
1395
        // close with the latest state, we check if it is the remote that
1396
        // closed with any prior or current state.
1397
        ok, err = c.handleKnownRemoteState(
3✔
1398
                commitSpend, broadcastStateNum, chainSet,
3✔
1399
        )
3✔
1400
        if err != nil {
3✔
1401
                return fmt.Errorf("handle known remote state: %w", err)
×
1402
        }
×
1403
        if ok {
6✔
1404
                return nil
3✔
1405
        }
3✔
1406

1407
        // Next, we'll check to see if this is a cooperative channel closure or
1408
        // not. This is characterized by having an input sequence number that's
1409
        // finalized. This won't happen with regular commitment transactions
1410
        // due to the state hint encoding scheme.
1411
        switch commitTxBroadcast.TxIn[0].Sequence {
3✔
1412
        case wire.MaxTxInSequenceNum:
3✔
1413
                fallthrough
3✔
1414
        case mempool.MaxRBFSequence:
3✔
1415
                // TODO(roasbeef): rare but possible, need itest case for
3✔
1416
                err := c.dispatchCooperativeClose(commitSpend)
3✔
1417
                if err != nil {
3✔
1418
                        return fmt.Errorf("handle coop close: %w", err)
×
1419
                }
×
1420

1421
                return nil
3✔
1422
        }
1423

1424
        log.Warnf("Unknown commitment broadcast for ChannelPoint(%v) ",
3✔
1425
                c.cfg.chanState.FundingOutpoint)
3✔
1426

3✔
1427
        // We'll try to recover as best as possible from losing state.  We
3✔
1428
        // first check if this was a local unknown state. This could happen if
3✔
1429
        // we force close, then lose state or attempt recovery before the
3✔
1430
        // commitment confirms.
3✔
1431
        ok, err = c.handleUnknownLocalState(
3✔
1432
                commitSpend, broadcastStateNum, chainSet,
3✔
1433
        )
3✔
1434
        if err != nil {
3✔
1435
                return fmt.Errorf("handle known local state: %w", err)
×
1436
        }
×
1437
        if ok {
6✔
1438
                return nil
3✔
1439
        }
3✔
1440

1441
        // Since it was neither a known remote state, nor a local state that
1442
        // was published, it most likely mean we lost state and the remote node
1443
        // closed. In this case we must start the DLP protocol in hope of
1444
        // getting our money back.
1445
        ok, err = c.handleUnknownRemoteState(
3✔
1446
                commitSpend, broadcastStateNum, chainSet,
3✔
1447
        )
3✔
1448
        if err != nil {
3✔
1449
                return fmt.Errorf("handle unknown remote state: %w", err)
×
1450
        }
×
1451
        if ok {
6✔
1452
                return nil
3✔
1453
        }
3✔
1454

1455
        log.Errorf("Unable to handle spending tx %v of channel point %v",
×
1456
                commitTxBroadcast.TxHash(), c.cfg.chanState.FundingOutpoint)
×
1457

×
1458
        return nil
×
1459
}
1460

1461
// checkFundingSpend performs a non-blocking read on the spendNtfn channel to
1462
// check whether there's a commit spend already. Returns the spend details if
1463
// found.
1464
func (c *chainWatcher) checkFundingSpend() *chainntnfs.SpendDetail {
3✔
1465
        select {
3✔
1466
        // We've detected a spend of the channel onchain! Depending on the type
1467
        // of spend, we'll act accordingly, so we'll examine the spending
1468
        // transaction to determine what we should do.
1469
        //
1470
        // TODO(Roasbeef): need to be able to ensure this only triggers
1471
        // on confirmation, to ensure if multiple txns are broadcast, we
1472
        // act on the one that's timestamped
1473
        case spend, ok := <-c.fundingSpendNtfn.Spend:
×
1474
                // If the channel was closed, then this means that the notifier
×
1475
                // exited, so we will as well.
×
1476
                if !ok {
×
1477
                        return nil
×
1478
                }
×
1479

1480
                log.Debugf("Found spend details for funding output: %v",
×
1481
                        spend.SpenderTxHash)
×
1482

×
1483
                return spend
×
1484

1485
        default:
3✔
1486
        }
1487

1488
        return nil
3✔
1489
}
1490

1491
// chanPointConfirmed checks whether the given channel point has confirmed.
1492
// This is used to ensure that the funding output has confirmed on chain before
1493
// we proceed with the rest of the close observer logic for taproot channels.
1494
// Check the docs in `fundingConfirmedNtfn` for details.
1495
func (c *chainWatcher) chanPointConfirmed() bool {
3✔
1496
        op := c.cfg.chanState.FundingOutpoint
3✔
1497

3✔
1498
        select {
3✔
1499
        case _, ok := <-c.fundingConfirmedNtfn.Confirmed:
3✔
1500
                // If the channel was closed, then this means that the notifier
3✔
1501
                // exited, so we will as well.
3✔
1502
                if !ok {
6✔
1503
                        return false
3✔
1504
                }
3✔
1505

1506
                log.Debugf("Taproot ChannelPoint(%v) confirmed", op)
3✔
1507

3✔
1508
                // The channel point has confirmed on chain. We now cancel the
3✔
1509
                // subscription.
3✔
1510
                c.fundingConfirmedNtfn.Cancel()
3✔
1511

3✔
1512
                return true
3✔
1513

1514
        default:
3✔
1515
                log.Infof("Taproot ChannelPoint(%v) not confirmed yet", op)
3✔
1516

3✔
1517
                return false
3✔
1518
        }
1519
}
1520

1521
// handleBlockbeat takes a blockbeat and queries for a spending tx for the
1522
// funding output. If the spending tx is found, it will be handled based on the
1523
// closure type.
1524
func (c *chainWatcher) handleBlockbeat(beat chainio.Blockbeat) {
3✔
1525
        // Notify the chain watcher has processed the block.
3✔
1526
        defer c.NotifyBlockProcessed(beat, nil)
3✔
1527

3✔
1528
        // If we have a fundingConfirmedNtfn, it means this is a taproot
3✔
1529
        // channel that is pending, before we proceed, we want to ensure that
3✔
1530
        // the expected funding output has confirmed on chain. Check the docs
3✔
1531
        // in `fundingConfirmedNtfn` for details.
3✔
1532
        if c.fundingConfirmedNtfn != nil {
6✔
1533
                // If the funding output hasn't confirmed in this block, we
3✔
1534
                // will check it again in the next block.
3✔
1535
                if !c.chanPointConfirmed() {
6✔
1536
                        return
3✔
1537
                }
3✔
1538
        }
1539

1540
        // Perform a non-blocking read to check whether the funding output was
1541
        // spent.
1542
        spend := c.checkFundingSpend()
3✔
1543
        if spend == nil {
6✔
1544
                log.Tracef("No spend found for ChannelPoint(%v) in block %v",
3✔
1545
                        c.cfg.chanState.FundingOutpoint, beat.Height())
3✔
1546

3✔
1547
                return
3✔
1548
        }
3✔
1549

1550
        // The funding output was spent, we now handle it by sending a close
1551
        // event to the channel arbitrator.
1552
        err := c.handleCommitSpend(spend)
×
1553
        if err != nil {
×
1554
                log.Errorf("Failed to handle commit spend: %v", err)
×
1555
        }
×
1556
}
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