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

lightningnetwork / lnd / 15561477203

10 Jun 2025 01:54PM UTC coverage: 58.351% (-10.1%) from 68.487%
15561477203

Pull #9356

github

web-flow
Merge 6440b25db into c6d6d4c0b
Pull Request #9356: lnrpc: add incoming/outgoing channel ids filter to forwarding history request

33 of 36 new or added lines in 2 files covered. (91.67%)

28366 existing lines in 455 files now uncovered.

97715 of 167461 relevant lines covered (58.35%)

1.81 hits per line

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

72.63
/contractcourt/breach_arbitrator.go
1
package contractcourt
2

3
import (
4
        "bytes"
5
        "encoding/binary"
6
        "errors"
7
        "fmt"
8
        "io"
9
        "sync"
10

11
        "github.com/btcsuite/btcd/blockchain"
12
        "github.com/btcsuite/btcd/btcutil"
13
        "github.com/btcsuite/btcd/chaincfg/chainhash"
14
        "github.com/btcsuite/btcd/txscript"
15
        "github.com/btcsuite/btcd/wire"
16
        "github.com/lightningnetwork/lnd/chainntnfs"
17
        "github.com/lightningnetwork/lnd/channeldb"
18
        "github.com/lightningnetwork/lnd/fn/v2"
19
        graphdb "github.com/lightningnetwork/lnd/graph/db"
20
        "github.com/lightningnetwork/lnd/input"
21
        "github.com/lightningnetwork/lnd/kvdb"
22
        "github.com/lightningnetwork/lnd/labels"
23
        "github.com/lightningnetwork/lnd/lntypes"
24
        "github.com/lightningnetwork/lnd/lnutils"
25
        "github.com/lightningnetwork/lnd/lnwallet"
26
        "github.com/lightningnetwork/lnd/lnwallet/chainfee"
27
        "github.com/lightningnetwork/lnd/sweep"
28
        "github.com/lightningnetwork/lnd/tlv"
29
)
30

31
const (
32
        // justiceTxConfTarget is the number of blocks we'll use as a
33
        // confirmation target when creating the justice transaction. We'll
34
        // choose an aggressive target, since we want to be sure it confirms
35
        // quickly.
36
        justiceTxConfTarget = 2
37

38
        // blocksPassedSplitPublish is the number of blocks without
39
        // confirmation of the justice tx we'll wait before starting to publish
40
        // smaller variants of the justice tx. We do this to mitigate an attack
41
        // the channel peer can do by pinning the HTLC outputs of the
42
        // commitment with low-fee HTLC transactions.
43
        blocksPassedSplitPublish = 4
44
)
45

46
var (
47
        // retributionBucket stores retribution state on disk between detecting
48
        // a contract breach, broadcasting a justice transaction that sweeps the
49
        // channel, and finally witnessing the justice transaction confirm on
50
        // the blockchain. It is critical that such state is persisted on disk,
51
        // so that if our node restarts at any point during the retribution
52
        // procedure, we can recover and continue from the persisted state.
53
        retributionBucket = []byte("retribution")
54

55
        // taprootRetributionBucket stores the tarpoot specific retribution
56
        // information. This includes things like the control blocks for both
57
        // commitment outputs, and the taptweak needed to sweep each HTLC (one
58
        // for the first and one for the second level).
59
        taprootRetributionBucket = []byte("tap-retribution")
60

61
        // errBrarShuttingDown is an error returned if the BreachArbitrator has
62
        // been signalled to exit.
63
        errBrarShuttingDown = errors.New("BreachArbitrator shutting down")
64
)
65

66
// ContractBreachEvent is an event the BreachArbitrator will receive in case a
67
// contract breach is observed on-chain. It contains the necessary information
68
// to handle the breach, and a ProcessACK closure we will use to ACK the event
69
// when we have safely stored all the necessary information.
70
type ContractBreachEvent struct {
71
        // ChanPoint is the channel point of the breached channel.
72
        ChanPoint wire.OutPoint
73

74
        // ProcessACK is an closure that should be called with a nil error iff
75
        // the breach retribution info is safely stored in the retribution
76
        // store. In case storing the information to the store fails, a non-nil
77
        // error should be used. When this closure returns, it means that the
78
        // contract court has marked the channel pending close in the DB, and
79
        // it is safe for the BreachArbitrator to carry on its duty.
80
        ProcessACK func(error)
81

82
        // BreachRetribution is the information needed to act on this contract
83
        // breach.
84
        BreachRetribution *lnwallet.BreachRetribution
85
}
86

87
// ChannelCloseType is an enum which signals the type of channel closure the
88
// peer should execute.
89
type ChannelCloseType uint8
90

91
const (
92
        // CloseRegular indicates a regular cooperative channel closure
93
        // should be attempted.
94
        CloseRegular ChannelCloseType = iota
95

96
        // CloseBreach indicates that a channel breach has been detected, and
97
        // the link should immediately be marked as unavailable.
98
        CloseBreach
99
)
100

101
// RetributionStorer provides an interface for managing a persistent map from
102
// wire.OutPoint -> retributionInfo. Upon learning of a breach, a
103
// BreachArbitrator should record the retributionInfo for the breached channel,
104
// which serves a checkpoint in the event that retribution needs to be resumed
105
// after failure. A RetributionStore provides an interface for managing the
106
// persisted set, as well as mapping user defined functions over the entire
107
// on-disk contents.
108
//
109
// Calls to RetributionStore may occur concurrently. A concrete instance of
110
// RetributionStore should use appropriate synchronization primitives, or
111
// be otherwise safe for concurrent access.
112
type RetributionStorer interface {
113
        // Add persists the retributionInfo to disk, using the information's
114
        // chanPoint as the key. This method should overwrite any existing
115
        // entries found under the same key, and an error should be raised if
116
        // the addition fails.
117
        Add(retInfo *retributionInfo) error
118

119
        // IsBreached queries the retribution store to see if the breach arbiter
120
        // is aware of any breaches for the provided channel point.
121
        IsBreached(chanPoint *wire.OutPoint) (bool, error)
122

123
        // Remove deletes the retributionInfo from disk, if any exists, under
124
        // the given key. An error should be re raised if the removal fails.
125
        Remove(key *wire.OutPoint) error
126

127
        // ForAll iterates over the existing on-disk contents and applies a
128
        // chosen, read-only callback to each. This method should ensure that it
129
        // immediately propagate any errors generated by the callback.
130
        ForAll(cb func(*retributionInfo) error, reset func()) error
131
}
132

133
// BreachConfig bundles the required subsystems used by the breach arbiter. An
134
// instance of BreachConfig is passed to NewBreachArbitrator during
135
// instantiation.
136
type BreachConfig struct {
137
        // CloseLink allows the breach arbiter to shutdown any channel links for
138
        // which it detects a breach, ensuring now further activity will
139
        // continue across the link. The method accepts link's channel point and
140
        // a close type to be included in the channel close summary.
141
        CloseLink func(*wire.OutPoint, ChannelCloseType)
142

143
        // DB provides access to the user's channels, allowing the breach
144
        // arbiter to determine the current state of a user's channels, and how
145
        // it should respond to channel closure.
146
        DB *channeldb.ChannelStateDB
147

148
        // Estimator is used by the breach arbiter to determine an appropriate
149
        // fee level when generating, signing, and broadcasting sweep
150
        // transactions.
151
        Estimator chainfee.Estimator
152

153
        // GenSweepScript generates the receiving scripts for swept outputs.
154
        GenSweepScript func() fn.Result[lnwallet.AddrWithKey]
155

156
        // Notifier provides a publish/subscribe interface for event driven
157
        // notifications regarding the confirmation of txids.
158
        Notifier chainntnfs.ChainNotifier
159

160
        // PublishTransaction facilitates the process of broadcasting a
161
        // transaction to the network.
162
        PublishTransaction func(*wire.MsgTx, string) error
163

164
        // ContractBreaches is a channel where the BreachArbitrator will receive
165
        // notifications in the event of a contract breach being observed. A
166
        // ContractBreachEvent must be ACKed by the BreachArbitrator, such that
167
        // the sending subsystem knows that the event is properly handed off.
168
        ContractBreaches <-chan *ContractBreachEvent
169

170
        // Signer is used by the breach arbiter to generate sweep transactions,
171
        // which move coins from previously open channels back to the user's
172
        // wallet.
173
        Signer input.Signer
174

175
        // Store is a persistent resource that maintains information regarding
176
        // breached channels. This is used in conjunction with DB to recover
177
        // from crashes, restarts, or other failures.
178
        Store RetributionStorer
179

180
        // AuxSweeper is an optional interface that can be used to modify the
181
        // way sweep transaction are generated.
182
        AuxSweeper fn.Option[sweep.AuxSweeper]
183
}
184

185
// BreachArbitrator is a special subsystem which is responsible for watching and
186
// acting on the detection of any attempted uncooperative channel breaches by
187
// channel counterparties. This file essentially acts as deterrence code for
188
// those attempting to launch attacks against the daemon. In practice it's
189
// expected that the logic in this file never gets executed, but it is
190
// important to have it in place just in case we encounter cheating channel
191
// counterparties.
192
// TODO(roasbeef): closures in config for subsystem pointers to decouple?
193
type BreachArbitrator struct {
194
        started sync.Once
195
        stopped sync.Once
196

197
        cfg *BreachConfig
198

199
        subscriptions map[wire.OutPoint]chan struct{}
200

201
        quit chan struct{}
202
        wg   sync.WaitGroup
203
        sync.Mutex
204
}
205

206
// NewBreachArbitrator creates a new instance of a BreachArbitrator initialized
207
// with its dependent objects.
208
func NewBreachArbitrator(cfg *BreachConfig) *BreachArbitrator {
3✔
209
        return &BreachArbitrator{
3✔
210
                cfg:           cfg,
3✔
211
                subscriptions: make(map[wire.OutPoint]chan struct{}),
3✔
212
                quit:          make(chan struct{}),
3✔
213
        }
3✔
214
}
3✔
215

216
// Start is an idempotent method that officially starts the BreachArbitrator
217
// along with all other goroutines it needs to perform its functions.
218
func (b *BreachArbitrator) Start() error {
3✔
219
        var err error
3✔
220
        b.started.Do(func() {
6✔
221
                brarLog.Info("Breach arbiter starting")
3✔
222
                err = b.start()
3✔
223
        })
3✔
224
        return err
3✔
225
}
226

227
func (b *BreachArbitrator) start() error {
3✔
228
        // Load all retributions currently persisted in the retribution store.
3✔
229
        var breachRetInfos map[wire.OutPoint]retributionInfo
3✔
230
        if err := b.cfg.Store.ForAll(func(ret *retributionInfo) error {
6✔
231
                breachRetInfos[ret.chanPoint] = *ret
3✔
232
                return nil
3✔
233
        }, func() {
6✔
234
                breachRetInfos = make(map[wire.OutPoint]retributionInfo)
3✔
235
        }); err != nil {
3✔
236
                brarLog.Errorf("Unable to create retribution info: %v", err)
×
237
                return err
×
238
        }
×
239

240
        // Load all currently closed channels from disk, we will use the
241
        // channels that have been marked fully closed to filter the retribution
242
        // information loaded from disk. This is necessary in the event that the
243
        // channel was marked fully closed, but was not removed from the
244
        // retribution store.
245
        closedChans, err := b.cfg.DB.FetchClosedChannels(false)
3✔
246
        if err != nil {
3✔
247
                brarLog.Errorf("Unable to fetch closing channels: %v", err)
×
248
                return err
×
249
        }
×
250

251
        brarLog.Debugf("Found %v closing channels, %v retribution records",
3✔
252
                len(closedChans), len(breachRetInfos))
3✔
253

3✔
254
        // Using the set of non-pending, closed channels, reconcile any
3✔
255
        // discrepancies between the channeldb and the retribution store by
3✔
256
        // removing any retribution information for which we have already
3✔
257
        // finished our responsibilities. If the removal is successful, we also
3✔
258
        // remove the entry from our in-memory map, to avoid any further action
3✔
259
        // for this channel.
3✔
260
        // TODO(halseth): no need continue on IsPending once closed channels
3✔
261
        // actually means close transaction is confirmed.
3✔
262
        for _, chanSummary := range closedChans {
6✔
263
                brarLog.Debugf("Working on close channel: %v, is_pending: %v",
3✔
264
                        chanSummary.ChanPoint, chanSummary.IsPending)
3✔
265

3✔
266
                if chanSummary.IsPending {
6✔
267
                        continue
3✔
268
                }
269

270
                chanPoint := &chanSummary.ChanPoint
3✔
271
                if _, ok := breachRetInfos[*chanPoint]; ok {
3✔
272
                        if err := b.cfg.Store.Remove(chanPoint); err != nil {
×
273
                                brarLog.Errorf("Unable to remove closed "+
×
274
                                        "chanid=%v from breach arbiter: %v",
×
275
                                        chanPoint, err)
×
276
                                return err
×
277
                        }
×
278
                        delete(breachRetInfos, *chanPoint)
×
279

×
280
                        brarLog.Debugf("Skipped closed channel: %v",
×
281
                                chanSummary.ChanPoint)
×
282
                }
283
        }
284

285
        // Spawn the exactRetribution tasks to monitor and resolve any breaches
286
        // that were loaded from the retribution store.
287
        for chanPoint := range breachRetInfos {
6✔
288
                retInfo := breachRetInfos[chanPoint]
3✔
289

3✔
290
                brarLog.Debugf("Handling breach handoff on startup "+
3✔
291
                        "for ChannelPoint(%v)", chanPoint)
3✔
292

3✔
293
                // Register for a notification when the breach transaction is
3✔
294
                // confirmed on chain.
3✔
295
                breachTXID := retInfo.commitHash
3✔
296
                breachScript := retInfo.breachedOutputs[0].signDesc.Output.PkScript
3✔
297
                confChan, err := b.cfg.Notifier.RegisterConfirmationsNtfn(
3✔
298
                        &breachTXID, breachScript, 1, retInfo.breachHeight,
3✔
299
                )
3✔
300
                if err != nil {
3✔
301
                        brarLog.Errorf("Unable to register for conf updates "+
×
302
                                "for txid: %v, err: %v", breachTXID, err)
×
303
                        return err
×
304
                }
×
305

306
                // Launch a new goroutine which to finalize the channel
307
                // retribution after the breach transaction confirms.
308
                b.wg.Add(1)
3✔
309
                go b.exactRetribution(confChan, &retInfo)
3✔
310
        }
311

312
        // Start watching the remaining active channels!
313
        b.wg.Add(1)
3✔
314
        go b.contractObserver()
3✔
315

3✔
316
        return nil
3✔
317
}
318

319
// Stop is an idempotent method that signals the BreachArbitrator to execute a
320
// graceful shutdown. This function will block until all goroutines spawned by
321
// the BreachArbitrator have gracefully exited.
322
func (b *BreachArbitrator) Stop() error {
3✔
323
        b.stopped.Do(func() {
6✔
324
                brarLog.Infof("Breach arbiter shutting down...")
3✔
325
                defer brarLog.Debug("Breach arbiter shutdown complete")
3✔
326

3✔
327
                close(b.quit)
3✔
328
                b.wg.Wait()
3✔
329
        })
3✔
330
        return nil
3✔
331
}
332

333
// IsBreached queries the breach arbiter's retribution store to see if it is
334
// aware of any channel breaches for a particular channel point.
UNCOV
335
func (b *BreachArbitrator) IsBreached(chanPoint *wire.OutPoint) (bool, error) {
×
UNCOV
336
        return b.cfg.Store.IsBreached(chanPoint)
×
UNCOV
337
}
×
338

339
// SubscribeBreachComplete is used by outside subsystems to be notified of a
340
// successful breach resolution.
341
func (b *BreachArbitrator) SubscribeBreachComplete(chanPoint *wire.OutPoint,
342
        c chan struct{}) (bool, error) {
3✔
343

3✔
344
        breached, err := b.cfg.Store.IsBreached(chanPoint)
3✔
345
        if err != nil {
3✔
346
                // If an error occurs, no subscription will be registered.
×
347
                return false, err
×
348
        }
×
349

350
        if !breached {
5✔
351
                // If chanPoint no longer exists in the Store, then the breach
2✔
352
                // was cleaned up successfully. Any subscription that occurs
2✔
353
                // happens after the breach information was persisted to the
2✔
354
                // underlying store.
2✔
355
                return true, nil
2✔
356
        }
2✔
357

358
        // Otherwise since the channel point is not resolved, add a
359
        // subscription. There can only be one subscription per channel point.
360
        b.Lock()
3✔
361
        defer b.Unlock()
3✔
362
        b.subscriptions[*chanPoint] = c
3✔
363

3✔
364
        return false, nil
3✔
365
}
366

367
// notifyBreachComplete is used by the BreachArbitrator to notify outside
368
// subsystems that the breach resolution process is complete.
369
func (b *BreachArbitrator) notifyBreachComplete(chanPoint *wire.OutPoint) {
3✔
370
        b.Lock()
3✔
371
        defer b.Unlock()
3✔
372
        if c, ok := b.subscriptions[*chanPoint]; ok {
6✔
373
                close(c)
3✔
374
        }
3✔
375

376
        // Remove the subscription.
377
        delete(b.subscriptions, *chanPoint)
3✔
378
}
379

380
// contractObserver is the primary goroutine for the BreachArbitrator. This
381
// goroutine is responsible for handling breach events coming from the
382
// contractcourt on the ContractBreaches channel. If a channel breach is
383
// detected, then the contractObserver will execute the retribution logic
384
// required to sweep ALL outputs from a contested channel into the daemon's
385
// wallet.
386
//
387
// NOTE: This MUST be run as a goroutine.
388
func (b *BreachArbitrator) contractObserver() {
3✔
389
        defer b.wg.Done()
3✔
390

3✔
391
        brarLog.Infof("Starting contract observer, watching for breaches.")
3✔
392

3✔
393
        for {
6✔
394
                select {
3✔
395
                case breachEvent := <-b.cfg.ContractBreaches:
3✔
396
                        // We have been notified about a contract breach!
3✔
397
                        // Handle the handoff, making sure we ACK the event
3✔
398
                        // after we have safely added it to the retribution
3✔
399
                        // store.
3✔
400
                        b.wg.Add(1)
3✔
401
                        go b.handleBreachHandoff(breachEvent)
3✔
402

403
                case <-b.quit:
3✔
404
                        return
3✔
405
                }
406
        }
407
}
408

409
// spend is used to wrap the index of the retributionInfo output that gets
410
// spent together with the spend details.
411
type spend struct {
412
        index  int
413
        detail *chainntnfs.SpendDetail
414
}
415

416
// waitForSpendEvent waits for any of the breached outputs to get spent, and
417
// returns the spend details for those outputs. The spendNtfns map is a cache
418
// used to store registered spend subscriptions, in case we must call this
419
// method multiple times.
420
func (b *BreachArbitrator) waitForSpendEvent(breachInfo *retributionInfo,
421
        spendNtfns map[wire.OutPoint]*chainntnfs.SpendEvent) ([]spend, error) {
3✔
422

3✔
423
        inputs := breachInfo.breachedOutputs
3✔
424

3✔
425
        // We create a channel the first goroutine that gets a spend event can
3✔
426
        // signal. We make it buffered in case multiple spend events come in at
3✔
427
        // the same time.
3✔
428
        anySpend := make(chan struct{}, len(inputs))
3✔
429

3✔
430
        // The allSpends channel will be used to pass spend events from all the
3✔
431
        // goroutines that detects a spend before they are signalled to exit.
3✔
432
        allSpends := make(chan spend, len(inputs))
3✔
433

3✔
434
        // exit will be used to signal the goroutines that they can exit.
3✔
435
        exit := make(chan struct{})
3✔
436
        var wg sync.WaitGroup
3✔
437

3✔
438
        // We'll now launch a goroutine for each of the HTLC outputs, that will
3✔
439
        // signal the moment they detect a spend event.
3✔
440
        for i := range inputs {
6✔
441
                breachedOutput := &inputs[i]
3✔
442

3✔
443
                brarLog.Infof("Checking spend from %v(%v) for ChannelPoint(%v)",
3✔
444
                        breachedOutput.witnessType, breachedOutput.outpoint,
3✔
445
                        breachInfo.chanPoint)
3✔
446

3✔
447
                // If we have already registered for a notification for this
3✔
448
                // output, we'll reuse it.
3✔
449
                spendNtfn, ok := spendNtfns[breachedOutput.outpoint]
3✔
450
                if !ok {
6✔
451
                        var err error
3✔
452
                        spendNtfn, err = b.cfg.Notifier.RegisterSpendNtfn(
3✔
453
                                &breachedOutput.outpoint,
3✔
454
                                breachedOutput.signDesc.Output.PkScript,
3✔
455
                                breachInfo.breachHeight,
3✔
456
                        )
3✔
457
                        if err != nil {
3✔
458
                                brarLog.Errorf("Unable to check for spentness "+
×
459
                                        "of outpoint=%v: %v",
×
460
                                        breachedOutput.outpoint, err)
×
461

×
462
                                // Registration may have failed if we've been
×
463
                                // instructed to shutdown. If so, return here
×
464
                                // to avoid entering an infinite loop.
×
465
                                select {
×
466
                                case <-b.quit:
×
467
                                        return nil, errBrarShuttingDown
×
468
                                default:
×
469
                                        continue
×
470
                                }
471
                        }
472
                        spendNtfns[breachedOutput.outpoint] = spendNtfn
3✔
473
                }
474

475
                // Launch a goroutine waiting for a spend event.
476
                b.wg.Add(1)
3✔
477
                wg.Add(1)
3✔
478
                go func(index int, spendEv *chainntnfs.SpendEvent) {
6✔
479
                        defer b.wg.Done()
3✔
480
                        defer wg.Done()
3✔
481

3✔
482
                        select {
3✔
483
                        // The output has been taken to the second level!
484
                        case sp, ok := <-spendEv.Spend:
3✔
485
                                if !ok {
3✔
486
                                        return
×
487
                                }
×
488

489
                                brarLog.Infof("Detected spend on %s(%v) by "+
3✔
490
                                        "txid(%v) for ChannelPoint(%v)",
3✔
491
                                        inputs[index].witnessType,
3✔
492
                                        inputs[index].outpoint,
3✔
493
                                        sp.SpenderTxHash,
3✔
494
                                        breachInfo.chanPoint)
3✔
495

3✔
496
                                // First we send the spend event on the
3✔
497
                                // allSpends channel, such that it can be
3✔
498
                                // handled after all go routines have exited.
3✔
499
                                allSpends <- spend{index, sp}
3✔
500

3✔
501
                                // Finally we'll signal the anySpend channel
3✔
502
                                // that a spend was detected, such that the
3✔
503
                                // other goroutines can be shut down.
3✔
504
                                anySpend <- struct{}{}
3✔
505
                        case <-exit:
3✔
506
                                return
3✔
507
                        case <-b.quit:
3✔
508
                                return
3✔
509
                        }
510
                }(i, spendNtfn)
511
        }
512

513
        // We'll wait for any of the outputs to be spent, or that we are
514
        // signalled to exit.
515
        select {
3✔
516
        // A goroutine have signalled that a spend occurred.
517
        case <-anySpend:
3✔
518
                // Signal for the remaining goroutines to exit.
3✔
519
                close(exit)
3✔
520
                wg.Wait()
3✔
521

3✔
522
                // At this point all goroutines that can send on the allSpends
3✔
523
                // channel have exited. We can therefore safely close the
3✔
524
                // channel before ranging over its content.
3✔
525
                close(allSpends)
3✔
526

3✔
527
                // Gather all detected spends and return them.
3✔
528
                var spends []spend
3✔
529
                for s := range allSpends {
6✔
530
                        breachedOutput := &inputs[s.index]
3✔
531
                        delete(spendNtfns, breachedOutput.outpoint)
3✔
532

3✔
533
                        spends = append(spends, s)
3✔
534
                }
3✔
535

536
                return spends, nil
3✔
537

538
        case <-b.quit:
3✔
539
                return nil, errBrarShuttingDown
3✔
540
        }
541
}
542

543
// convertToSecondLevelRevoke takes a breached output, and a transaction that
544
// spends it to the second level, and mutates the breach output into one that
545
// is able to properly sweep that second level output. We'll use this function
546
// when we go to sweep a breached commitment transaction, but the cheating
547
// party has already attempted to take it to the second level.
548
func convertToSecondLevelRevoke(bo *breachedOutput, breachInfo *retributionInfo,
UNCOV
549
        spendDetails *chainntnfs.SpendDetail) {
×
UNCOV
550

×
UNCOV
551
        // In this case, we'll modify the witness type of this output to
×
UNCOV
552
        // actually prepare for a second level revoke.
×
UNCOV
553
        isTaproot := txscript.IsPayToTaproot(bo.signDesc.Output.PkScript)
×
UNCOV
554
        if isTaproot {
×
555
                bo.witnessType = input.TaprootHtlcSecondLevelRevoke
×
UNCOV
556
        } else {
×
UNCOV
557
                bo.witnessType = input.HtlcSecondLevelRevoke
×
UNCOV
558
        }
×
559

560
        // We'll also redirect the outpoint to this second level output, so the
561
        // spending transaction updates it inputs accordingly.
UNCOV
562
        spendingTx := spendDetails.SpendingTx
×
UNCOV
563
        spendInputIndex := spendDetails.SpenderInputIndex
×
UNCOV
564
        oldOp := bo.outpoint
×
UNCOV
565
        bo.outpoint = wire.OutPoint{
×
UNCOV
566
                Hash:  spendingTx.TxHash(),
×
UNCOV
567
                Index: spendInputIndex,
×
UNCOV
568
        }
×
UNCOV
569

×
UNCOV
570
        // Next, we need to update the amount so we can do fee estimation
×
UNCOV
571
        // properly, and also so we can generate a valid signature as we need
×
UNCOV
572
        // to know the new input value (the second level transactions shaves
×
UNCOV
573
        // off some funds to fees).
×
UNCOV
574
        newAmt := spendingTx.TxOut[spendInputIndex].Value
×
UNCOV
575
        bo.amt = btcutil.Amount(newAmt)
×
UNCOV
576
        bo.signDesc.Output.Value = newAmt
×
UNCOV
577
        bo.signDesc.Output.PkScript = spendingTx.TxOut[spendInputIndex].PkScript
×
UNCOV
578

×
UNCOV
579
        // For taproot outputs, the taptweak also needs to be swapped out. We
×
UNCOV
580
        // do this unconditionally as this field isn't used at all for segwit
×
UNCOV
581
        // v0 outputs.
×
UNCOV
582
        bo.signDesc.TapTweak = bo.secondLevelTapTweak[:]
×
UNCOV
583

×
UNCOV
584
        // Finally, we'll need to adjust the witness program in the
×
UNCOV
585
        // SignDescriptor.
×
UNCOV
586
        bo.signDesc.WitnessScript = bo.secondLevelWitnessScript
×
UNCOV
587

×
UNCOV
588
        brarLog.Warnf("HTLC(%v) for ChannelPoint(%v) has been spent to the "+
×
UNCOV
589
                "second-level, adjusting -> %v", oldOp, breachInfo.chanPoint,
×
UNCOV
590
                bo.outpoint)
×
591
}
592

593
// updateBreachInfo mutates the passed breachInfo by removing or converting any
594
// outputs among the spends. It also counts the total and revoked funds swept
595
// by our justice spends.
596
func updateBreachInfo(breachInfo *retributionInfo, spends []spend) (
597
        btcutil.Amount, btcutil.Amount) {
3✔
598

3✔
599
        inputs := breachInfo.breachedOutputs
3✔
600
        doneOutputs := make(map[int]struct{})
3✔
601

3✔
602
        var totalFunds, revokedFunds btcutil.Amount
3✔
603
        for _, s := range spends {
6✔
604
                breachedOutput := &inputs[s.index]
3✔
605
                txIn := s.detail.SpendingTx.TxIn[s.detail.SpenderInputIndex]
3✔
606

3✔
607
                switch breachedOutput.witnessType {
3✔
608
                case input.TaprootHtlcAcceptedRevoke:
3✔
609
                        fallthrough
3✔
610
                case input.TaprootHtlcOfferedRevoke:
3✔
611
                        fallthrough
3✔
612
                case input.HtlcAcceptedRevoke:
3✔
613
                        fallthrough
3✔
614
                case input.HtlcOfferedRevoke:
3✔
615
                        // If the HTLC output was spent using the revocation
3✔
616
                        // key, it is our own spend, and we can forget the
3✔
617
                        // output. Otherwise it has been taken to the second
3✔
618
                        // level.
3✔
619
                        signDesc := &breachedOutput.signDesc
3✔
620
                        ok, err := input.IsHtlcSpendRevoke(txIn, signDesc)
3✔
621
                        if err != nil {
3✔
622
                                brarLog.Errorf("Unable to determine if "+
×
623
                                        "revoke spend: %v", err)
×
624
                                break
×
625
                        }
626

627
                        if ok {
6✔
628
                                brarLog.Debugf("HTLC spend was our own " +
3✔
629
                                        "revocation spend")
3✔
630
                                break
3✔
631
                        }
632

UNCOV
633
                        brarLog.Infof("Spend on second-level "+
×
UNCOV
634
                                "%s(%v) for ChannelPoint(%v) "+
×
UNCOV
635
                                "transitions to second-level output",
×
UNCOV
636
                                breachedOutput.witnessType,
×
UNCOV
637
                                breachedOutput.outpoint, breachInfo.chanPoint)
×
UNCOV
638

×
UNCOV
639
                        // In this case we'll morph our initial revoke
×
UNCOV
640
                        // spend to instead point to the second level
×
UNCOV
641
                        // output, and update the sign descriptor in the
×
UNCOV
642
                        // process.
×
UNCOV
643
                        convertToSecondLevelRevoke(
×
UNCOV
644
                                breachedOutput, breachInfo, s.detail,
×
UNCOV
645
                        )
×
UNCOV
646

×
UNCOV
647
                        continue
×
648
                }
649

650
                // Now that we have determined the spend is done by us, we
651
                // count the total and revoked funds swept depending on the
652
                // input type.
653
                switch breachedOutput.witnessType {
3✔
654
                // If the output being revoked is the remote commitment output
655
                // or an offered HTLC output, its amount contributes to the
656
                // value of funds being revoked from the counter party.
657
                case input.CommitmentRevoke, input.TaprootCommitmentRevoke,
658
                        input.HtlcSecondLevelRevoke,
659
                        input.TaprootHtlcSecondLevelRevoke,
660
                        input.TaprootHtlcOfferedRevoke, input.HtlcOfferedRevoke:
3✔
661

3✔
662
                        revokedFunds += breachedOutput.Amount()
3✔
663
                }
664

665
                totalFunds += breachedOutput.Amount()
3✔
666
                brarLog.Infof("Spend on %s(%v) for ChannelPoint(%v) "+
3✔
667
                        "transitions output to terminal state, "+
3✔
668
                        "removing input from justice transaction",
3✔
669
                        breachedOutput.witnessType,
3✔
670
                        breachedOutput.outpoint, breachInfo.chanPoint)
3✔
671

3✔
672
                doneOutputs[s.index] = struct{}{}
3✔
673
        }
674

675
        // Filter the inputs for which we can no longer proceed.
676
        var nextIndex int
3✔
677
        for i := range inputs {
6✔
678
                if _, ok := doneOutputs[i]; ok {
6✔
679
                        continue
3✔
680
                }
681

682
                inputs[nextIndex] = inputs[i]
3✔
683
                nextIndex++
3✔
684
        }
685

686
        // Update our remaining set of outputs before continuing with
687
        // another attempt at publication.
688
        breachInfo.breachedOutputs = inputs[:nextIndex]
3✔
689
        return totalFunds, revokedFunds
3✔
690
}
691

692
// exactRetribution is a goroutine which is executed once a contract breach has
693
// been detected by a breachObserver. This function is responsible for
694
// punishing a counterparty for violating the channel contract by sweeping ALL
695
// the lingering funds within the channel into the daemon's wallet.
696
//
697
// NOTE: This MUST be run as a goroutine.
698
//
699
//nolint:funlen
700
func (b *BreachArbitrator) exactRetribution(
701
        confChan *chainntnfs.ConfirmationEvent, breachInfo *retributionInfo) {
3✔
702

3✔
703
        defer b.wg.Done()
3✔
704

3✔
705
        // TODO(roasbeef): state needs to be checkpointed here
3✔
706
        select {
3✔
707
        case _, ok := <-confChan.Confirmed:
3✔
708
                // If the second value is !ok, then the channel has been closed
3✔
709
                // signifying a daemon shutdown, so we exit.
3✔
710
                if !ok {
3✔
711
                        return
×
712
                }
×
713

714
                // Otherwise, if this is a real confirmation notification, then
715
                // we fall through to complete our duty.
UNCOV
716
        case <-b.quit:
×
UNCOV
717
                return
×
718
        }
719

720
        brarLog.Debugf("Breach transaction %v has been confirmed, sweeping "+
3✔
721
                "revoked funds", breachInfo.commitHash)
3✔
722

3✔
723
        // We may have to wait for some of the HTLC outputs to be spent to the
3✔
724
        // second level before broadcasting the justice tx. We'll store the
3✔
725
        // SpendEvents between each attempt to not re-register unnecessarily.
3✔
726
        spendNtfns := make(map[wire.OutPoint]*chainntnfs.SpendEvent)
3✔
727

3✔
728
        // Compute both the total value of funds being swept and the
3✔
729
        // amount of funds that were revoked from the counter party.
3✔
730
        var totalFunds, revokedFunds btcutil.Amount
3✔
731

3✔
732
justiceTxBroadcast:
3✔
733
        // With the breach transaction confirmed, we now create the
734
        // justice tx which will claim ALL the funds within the
735
        // channel.
736
        justiceTxs, err := b.createJusticeTx(breachInfo.breachedOutputs)
3✔
737
        if err != nil {
3✔
738
                brarLog.Errorf("Unable to create justice tx: %v", err)
×
739
                return
×
740
        }
×
741
        finalTx := justiceTxs.spendAll
3✔
742

3✔
743
        brarLog.Debugf("Broadcasting justice tx: %v", lnutils.SpewLogClosure(
3✔
744
                finalTx))
3✔
745

3✔
746
        // As we're about to broadcast our breach transaction, we'll notify the
3✔
747
        // aux sweeper of our broadcast attempt first.
3✔
748
        err = fn.MapOptionZ(b.cfg.AuxSweeper, func(aux sweep.AuxSweeper) error {
3✔
749
                bumpReq := sweep.BumpRequest{
×
750
                        Inputs:          finalTx.inputs,
×
751
                        DeliveryAddress: finalTx.sweepAddr,
×
752
                        ExtraTxOut:      finalTx.extraTxOut,
×
753
                }
×
754

×
755
                return aux.NotifyBroadcast(
×
756
                        &bumpReq, finalTx.justiceTx, finalTx.fee, nil,
×
757
                )
×
758
        })
×
759
        if err != nil {
3✔
760
                brarLog.Errorf("unable to notify broadcast: %w", err)
×
761
                return
×
762
        }
×
763

764
        // We'll now attempt to broadcast the transaction which finalized the
765
        // channel's retribution against the cheating counter party.
766
        label := labels.MakeLabel(labels.LabelTypeJusticeTransaction, nil)
3✔
767
        err = b.cfg.PublishTransaction(finalTx.justiceTx, label)
3✔
768
        if err != nil {
6✔
769
                brarLog.Errorf("Unable to broadcast justice tx: %v", err)
3✔
770
        }
3✔
771

772
        // Regardless of publication succeeded or not, we now wait for any of
773
        // the inputs to be spent. If any input got spent by the remote, we
774
        // must recreate our justice transaction.
775
        var (
3✔
776
                spendChan = make(chan []spend, 1)
3✔
777
                errChan   = make(chan error, 1)
3✔
778
                wg        sync.WaitGroup
3✔
779
        )
3✔
780

3✔
781
        wg.Add(1)
3✔
782
        go func() {
6✔
783
                defer wg.Done()
3✔
784

3✔
785
                spends, err := b.waitForSpendEvent(breachInfo, spendNtfns)
3✔
786
                if err != nil {
6✔
787
                        errChan <- err
3✔
788
                        return
3✔
789
                }
3✔
790
                spendChan <- spends
3✔
791
        }()
792

793
        // We'll also register for block notifications, such that in case our
794
        // justice tx doesn't confirm within a reasonable timeframe, we can
795
        // start to more aggressively sweep the time sensitive outputs.
796
        newBlockChan, err := b.cfg.Notifier.RegisterBlockEpochNtfn(nil)
3✔
797
        if err != nil {
3✔
798
                brarLog.Errorf("Unable to register for block notifications: %v",
×
799
                        err)
×
800
                return
×
801
        }
×
802
        defer newBlockChan.Cancel()
3✔
803

3✔
804
Loop:
3✔
805
        for {
6✔
806
                select {
3✔
807
                case spends := <-spendChan:
3✔
808
                        // Update the breach info with the new spends.
3✔
809
                        t, r := updateBreachInfo(breachInfo, spends)
3✔
810
                        totalFunds += t
3✔
811
                        revokedFunds += r
3✔
812

3✔
813
                        brarLog.Infof("%v spends from breach tx for "+
3✔
814
                                "ChannelPoint(%v) has been detected, %v "+
3✔
815
                                "revoked funds (%v total) have been claimed",
3✔
816
                                len(spends), breachInfo.chanPoint,
3✔
817
                                revokedFunds, totalFunds)
3✔
818

3✔
819
                        if len(breachInfo.breachedOutputs) == 0 {
6✔
820
                                brarLog.Infof("Justice for ChannelPoint(%v) "+
3✔
821
                                        "has been served, %v revoked funds "+
3✔
822
                                        "(%v total) have been claimed. No "+
3✔
823
                                        "more outputs to sweep, marking fully "+
3✔
824
                                        "resolved", breachInfo.chanPoint,
3✔
825
                                        revokedFunds, totalFunds)
3✔
826

3✔
827
                                err = b.cleanupBreach(&breachInfo.chanPoint)
3✔
828
                                if err != nil {
3✔
829
                                        brarLog.Errorf("Failed to cleanup "+
×
830
                                                "breached ChannelPoint(%v): %v",
×
831
                                                breachInfo.chanPoint, err)
×
832
                                }
×
833

834
                                // TODO(roasbeef): add peer to blacklist?
835

836
                                // TODO(roasbeef): close other active channels
837
                                // with offending peer
838
                                break Loop
3✔
839
                        }
840

841
                        brarLog.Infof("Attempting another justice tx "+
3✔
842
                                "with %d inputs",
3✔
843
                                len(breachInfo.breachedOutputs))
3✔
844

3✔
845
                        wg.Wait()
3✔
846
                        goto justiceTxBroadcast
3✔
847

848
                // On every new block, we check whether we should republish the
849
                // transactions.
850
                case epoch, ok := <-newBlockChan.Epochs:
3✔
851
                        if !ok {
3✔
852
                                return
×
853
                        }
×
854

855
                        // If less than four blocks have passed since the
856
                        // breach confirmed, we'll continue waiting. It was
857
                        // published with a 2-block fee estimate, so it's not
858
                        // unexpected that four blocks without confirmation can
859
                        // pass.
860
                        splitHeight := breachInfo.breachHeight +
3✔
861
                                blocksPassedSplitPublish
3✔
862
                        if uint32(epoch.Height) < splitHeight {
6✔
863
                                continue Loop
3✔
864
                        }
865

UNCOV
866
                        brarLog.Warnf("Block height %v arrived without "+
×
UNCOV
867
                                "justice tx confirming (breached at "+
×
UNCOV
868
                                "height %v), splitting justice tx.",
×
UNCOV
869
                                epoch.Height, breachInfo.breachHeight)
×
UNCOV
870

×
UNCOV
871
                        // Otherwise we'll attempt to publish the two separate
×
UNCOV
872
                        // justice transactions that sweeps the commitment
×
UNCOV
873
                        // outputs and the HTLC outputs separately. This is to
×
UNCOV
874
                        // mitigate the case where our "spend all" justice TX
×
UNCOV
875
                        // doesn't propagate because the HTLC outputs have been
×
UNCOV
876
                        // pinned by low fee HTLC txs.
×
UNCOV
877
                        label := labels.MakeLabel(
×
UNCOV
878
                                labels.LabelTypeJusticeTransaction, nil,
×
UNCOV
879
                        )
×
UNCOV
880
                        if justiceTxs.spendCommitOuts != nil {
×
UNCOV
881
                                tx := justiceTxs.spendCommitOuts
×
UNCOV
882

×
UNCOV
883
                                brarLog.Debugf("Broadcasting justice tx "+
×
UNCOV
884
                                        "spending commitment outs: %v",
×
UNCOV
885
                                        lnutils.SpewLogClosure(tx))
×
UNCOV
886

×
UNCOV
887
                                err = b.cfg.PublishTransaction(
×
UNCOV
888
                                        tx.justiceTx, label,
×
UNCOV
889
                                )
×
UNCOV
890
                                if err != nil {
×
891
                                        brarLog.Warnf("Unable to broadcast "+
×
892
                                                "commit out spending justice "+
×
893
                                                "tx: %v", err)
×
894
                                }
×
895
                        }
896

UNCOV
897
                        if justiceTxs.spendHTLCs != nil {
×
UNCOV
898
                                tx := justiceTxs.spendHTLCs
×
UNCOV
899

×
UNCOV
900
                                brarLog.Debugf("Broadcasting justice tx "+
×
UNCOV
901
                                        "spending HTLC outs: %v",
×
UNCOV
902
                                        lnutils.SpewLogClosure(tx))
×
UNCOV
903

×
UNCOV
904
                                err = b.cfg.PublishTransaction(
×
UNCOV
905
                                        tx.justiceTx, label,
×
UNCOV
906
                                )
×
UNCOV
907
                                if err != nil {
×
908
                                        brarLog.Warnf("Unable to broadcast "+
×
909
                                                "HTLC out spending justice "+
×
910
                                                "tx: %v", err)
×
911
                                }
×
912
                        }
913

UNCOV
914
                        for _, tx := range justiceTxs.spendSecondLevelHTLCs {
×
915
                                tx := tx
×
916

×
917
                                brarLog.Debugf("Broadcasting justice tx "+
×
918
                                        "spending second-level HTLC output: %v",
×
919
                                        lnutils.SpewLogClosure(tx))
×
920

×
921
                                err = b.cfg.PublishTransaction(
×
922
                                        tx.justiceTx, label,
×
923
                                )
×
924
                                if err != nil {
×
925
                                        brarLog.Warnf("Unable to broadcast "+
×
926
                                                "second-level HTLC out "+
×
927
                                                "spending justice tx: %v", err)
×
928
                                }
×
929
                        }
930

931
                case err := <-errChan:
×
932
                        if err != errBrarShuttingDown {
×
933
                                brarLog.Errorf("error waiting for "+
×
934
                                        "spend event: %v", err)
×
935
                        }
×
936
                        break Loop
×
937

938
                case <-b.quit:
3✔
939
                        break Loop
3✔
940
                }
941
        }
942

943
        // Wait for our go routine to exit.
944
        wg.Wait()
3✔
945
}
946

947
// cleanupBreach marks the given channel point as fully resolved and removes the
948
// retribution for that the channel from the retribution store.
949
func (b *BreachArbitrator) cleanupBreach(chanPoint *wire.OutPoint) error {
3✔
950
        // With the channel closed, mark it in the database as such.
3✔
951
        err := b.cfg.DB.MarkChanFullyClosed(chanPoint)
3✔
952
        if err != nil {
3✔
953
                return fmt.Errorf("unable to mark chan as closed: %w", err)
×
954
        }
×
955

956
        // Justice has been carried out; we can safely delete the retribution
957
        // info from the database.
958
        err = b.cfg.Store.Remove(chanPoint)
3✔
959
        if err != nil {
3✔
960
                return fmt.Errorf("unable to remove retribution from db: %w",
×
961
                        err)
×
962
        }
×
963

964
        // This is after the Remove call so that the chan passed in via
965
        // SubscribeBreachComplete is always notified, no matter when it is
966
        // called. Otherwise, if notifyBreachComplete was before Remove, a
967
        // very rare edge case could occur in which SubscribeBreachComplete
968
        // is called after notifyBreachComplete and before Remove, meaning the
969
        // caller would never be notified.
970
        b.notifyBreachComplete(chanPoint)
3✔
971

3✔
972
        return nil
3✔
973
}
974

975
// handleBreachHandoff handles a new breach event, by writing it to disk, then
976
// notifies the BreachArbitrator contract observer goroutine that a channel's
977
// contract has been breached by the prior counterparty. Once notified the
978
// BreachArbitrator will attempt to sweep ALL funds within the channel using the
979
// information provided within the BreachRetribution generated due to the
980
// breach of channel contract. The funds will be swept only after the breaching
981
// transaction receives a necessary number of confirmations.
982
//
983
// NOTE: This MUST be run as a goroutine.
984
func (b *BreachArbitrator) handleBreachHandoff(
985
        breachEvent *ContractBreachEvent) {
3✔
986

3✔
987
        defer b.wg.Done()
3✔
988

3✔
989
        chanPoint := breachEvent.ChanPoint
3✔
990
        brarLog.Debugf("Handling breach handoff for ChannelPoint(%v)",
3✔
991
                chanPoint)
3✔
992

3✔
993
        // A read from this channel indicates that a channel breach has been
3✔
994
        // detected! So we notify the main coordination goroutine with the
3✔
995
        // information needed to bring the counterparty to justice.
3✔
996
        breachInfo := breachEvent.BreachRetribution
3✔
997
        brarLog.Warnf("REVOKED STATE #%v FOR ChannelPoint(%v) "+
3✔
998
                "broadcast, REMOTE PEER IS DOING SOMETHING "+
3✔
999
                "SKETCHY!!!", breachInfo.RevokedStateNum,
3✔
1000
                chanPoint)
3✔
1001

3✔
1002
        // Immediately notify the HTLC switch that this link has been
3✔
1003
        // breached in order to ensure any incoming or outgoing
3✔
1004
        // multi-hop HTLCs aren't sent over this link, nor any other
3✔
1005
        // links associated with this peer.
3✔
1006
        b.cfg.CloseLink(&chanPoint, CloseBreach)
3✔
1007

3✔
1008
        // TODO(roasbeef): need to handle case of remote broadcast
3✔
1009
        // mid-local initiated state-transition, possible
3✔
1010
        // false-positive?
3✔
1011

3✔
1012
        // Acquire the mutex to ensure consistency between the call to
3✔
1013
        // IsBreached and Add below.
3✔
1014
        b.Lock()
3✔
1015

3✔
1016
        // We first check if this breach info is already added to the
3✔
1017
        // retribution store.
3✔
1018
        breached, err := b.cfg.Store.IsBreached(&chanPoint)
3✔
1019
        if err != nil {
3✔
1020
                b.Unlock()
×
1021
                brarLog.Errorf("Unable to check breach info in DB: %v", err)
×
1022

×
1023
                // Notify about the failed lookup and return.
×
1024
                breachEvent.ProcessACK(err)
×
1025
                return
×
1026
        }
×
1027

1028
        // If this channel is already marked as breached in the retribution
1029
        // store, we already have handled the handoff for this breach. In this
1030
        // case we can safely ACK the handoff, and return.
1031
        if breached {
3✔
UNCOV
1032
                b.Unlock()
×
UNCOV
1033
                breachEvent.ProcessACK(nil)
×
UNCOV
1034
                return
×
UNCOV
1035
        }
×
1036

1037
        // Using the breach information provided by the wallet and the
1038
        // channel snapshot, construct the retribution information that
1039
        // will be persisted to disk.
1040
        retInfo := newRetributionInfo(&chanPoint, breachInfo)
3✔
1041

3✔
1042
        // Persist the pending retribution state to disk.
3✔
1043
        err = b.cfg.Store.Add(retInfo)
3✔
1044
        b.Unlock()
3✔
1045
        if err != nil {
3✔
UNCOV
1046
                brarLog.Errorf("Unable to persist retribution "+
×
UNCOV
1047
                        "info to db: %v", err)
×
UNCOV
1048
        }
×
1049

1050
        // Now that the breach has been persisted, try to send an
1051
        // acknowledgment back to the close observer with the error. If
1052
        // the ack is successful, the close observer will mark the
1053
        // channel as pending-closed in the channeldb.
1054
        breachEvent.ProcessACK(err)
3✔
1055

3✔
1056
        // Bail if we failed to persist retribution info.
3✔
1057
        if err != nil {
3✔
UNCOV
1058
                return
×
UNCOV
1059
        }
×
1060

1061
        // Now that a new channel contract has been added to the retribution
1062
        // store, we first register for a notification to be dispatched once
1063
        // the breach transaction (the revoked commitment transaction) has been
1064
        // confirmed in the chain to ensure we're not dealing with a moving
1065
        // target.
1066
        breachTXID := &retInfo.commitHash
3✔
1067
        breachScript := retInfo.breachedOutputs[0].signDesc.Output.PkScript
3✔
1068
        cfChan, err := b.cfg.Notifier.RegisterConfirmationsNtfn(
3✔
1069
                breachTXID, breachScript, 1, retInfo.breachHeight,
3✔
1070
        )
3✔
1071
        if err != nil {
3✔
1072
                brarLog.Errorf("Unable to register for conf updates for "+
×
1073
                        "txid: %v, err: %v", breachTXID, err)
×
1074
                return
×
1075
        }
×
1076

1077
        brarLog.Warnf("A channel has been breached with txid: %v. Waiting "+
3✔
1078
                "for confirmation, then justice will be served!", breachTXID)
3✔
1079

3✔
1080
        // With the retribution state persisted, channel close persisted, and
3✔
1081
        // notification registered, we launch a new goroutine which will
3✔
1082
        // finalize the channel retribution after the breach transaction has
3✔
1083
        // been confirmed.
3✔
1084
        b.wg.Add(1)
3✔
1085
        go b.exactRetribution(cfChan, retInfo)
3✔
1086
}
1087

1088
// breachedOutput contains all the information needed to sweep a breached
1089
// output. A breached output is an output that we are now entitled to due to a
1090
// revoked commitment transaction being broadcast.
1091
type breachedOutput struct {
1092
        amt         btcutil.Amount
1093
        outpoint    wire.OutPoint
1094
        witnessType input.StandardWitnessType
1095
        signDesc    input.SignDescriptor
1096
        confHeight  uint32
1097

1098
        secondLevelWitnessScript []byte
1099
        secondLevelTapTweak      [32]byte
1100

1101
        witnessFunc input.WitnessGenerator
1102

1103
        resolutionBlob fn.Option[tlv.Blob]
1104

1105
        // TODO(roasbeef): function opt and hook into brar
1106
}
1107

1108
// makeBreachedOutput assembles a new breachedOutput that can be used by the
1109
// breach arbiter to construct a justice or sweep transaction.
1110
func makeBreachedOutput(outpoint *wire.OutPoint,
1111
        witnessType input.StandardWitnessType, secondLevelScript []byte,
1112
        signDescriptor *input.SignDescriptor, confHeight uint32,
1113
        resolutionBlob fn.Option[tlv.Blob]) breachedOutput {
3✔
1114

3✔
1115
        amount := signDescriptor.Output.Value
3✔
1116

3✔
1117
        return breachedOutput{
3✔
1118
                amt:                      btcutil.Amount(amount),
3✔
1119
                outpoint:                 *outpoint,
3✔
1120
                secondLevelWitnessScript: secondLevelScript,
3✔
1121
                witnessType:              witnessType,
3✔
1122
                signDesc:                 *signDescriptor,
3✔
1123
                confHeight:               confHeight,
3✔
1124
                resolutionBlob:           resolutionBlob,
3✔
1125
        }
3✔
1126
}
3✔
1127

1128
// Amount returns the number of satoshis contained in the breached output.
1129
func (bo *breachedOutput) Amount() btcutil.Amount {
3✔
1130
        return bo.amt
3✔
1131
}
3✔
1132

1133
// OutPoint returns the breached output's identifier that is to be included as a
1134
// transaction input.
1135
func (bo *breachedOutput) OutPoint() wire.OutPoint {
3✔
1136
        return bo.outpoint
3✔
1137
}
3✔
1138

1139
// RequiredTxOut returns a non-nil TxOut if input commits to a certain
1140
// transaction output. This is used in the SINGLE|ANYONECANPAY case to make
1141
// sure any presigned input is still valid by including the output.
1142
func (bo *breachedOutput) RequiredTxOut() *wire.TxOut {
×
1143
        return nil
×
1144
}
×
1145

1146
// RequiredLockTime returns whether this input commits to a tx locktime that
1147
// must be used in the transaction including it.
1148
func (bo *breachedOutput) RequiredLockTime() (uint32, bool) {
×
1149
        return 0, false
×
1150
}
×
1151

1152
// WitnessType returns the type of witness that must be generated to spend the
1153
// breached output.
1154
func (bo *breachedOutput) WitnessType() input.WitnessType {
3✔
1155
        return bo.witnessType
3✔
1156
}
3✔
1157

1158
// SignDesc returns the breached output's SignDescriptor, which is used during
1159
// signing to compute the witness.
1160
func (bo *breachedOutput) SignDesc() *input.SignDescriptor {
3✔
1161
        return &bo.signDesc
3✔
1162
}
3✔
1163

1164
// Preimage returns the preimage that was used to create the breached output.
1165
func (bo *breachedOutput) Preimage() fn.Option[lntypes.Preimage] {
×
1166
        return fn.None[lntypes.Preimage]()
×
1167
}
×
1168

1169
// CraftInputScript computes a valid witness that allows us to spend from the
1170
// breached output. It does so by first generating and memoizing the witness
1171
// generation function, which parameterized primarily by the witness type and
1172
// sign descriptor. The method then returns the witness computed by invoking
1173
// this function on the first and subsequent calls.
1174
func (bo *breachedOutput) CraftInputScript(signer input.Signer, txn *wire.MsgTx,
1175
        hashCache *txscript.TxSigHashes,
1176
        prevOutputFetcher txscript.PrevOutputFetcher,
1177
        txinIdx int) (*input.Script, error) {
3✔
1178

3✔
1179
        // First, we ensure that the witness generation function has been
3✔
1180
        // initialized for this breached output.
3✔
1181
        signDesc := bo.SignDesc()
3✔
1182
        signDesc.PrevOutputFetcher = prevOutputFetcher
3✔
1183
        bo.witnessFunc = bo.witnessType.WitnessGenerator(signer, signDesc)
3✔
1184

3✔
1185
        // Now that we have ensured that the witness generation function has
3✔
1186
        // been initialized, we can proceed to execute it and generate the
3✔
1187
        // witness for this particular breached output.
3✔
1188
        return bo.witnessFunc(txn, hashCache, txinIdx)
3✔
1189
}
3✔
1190

1191
// BlocksToMaturity returns the relative timelock, as a number of blocks, that
1192
// must be built on top of the confirmation height before the output can be
1193
// spent.
1194
func (bo *breachedOutput) BlocksToMaturity() uint32 {
3✔
1195
        // If the output is a to_remote output we can claim, and it's of the
3✔
1196
        // confirmed type (or is a taproot channel that always has the CSV 1),
3✔
1197
        // we must wait one block before claiming it.
3✔
1198
        switch bo.witnessType {
3✔
1199
        case input.CommitmentToRemoteConfirmed, input.TaprootRemoteCommitSpend:
3✔
1200
                return 1
3✔
1201
        }
1202

1203
        // All other breached outputs have no CSV delay.
1204
        return 0
3✔
1205
}
1206

1207
// HeightHint returns the minimum height at which a confirmed spending tx can
1208
// occur.
1209
func (bo *breachedOutput) HeightHint() uint32 {
×
1210
        return bo.confHeight
×
1211
}
×
1212

1213
// UnconfParent returns information about a possibly unconfirmed parent tx.
1214
func (bo *breachedOutput) UnconfParent() *input.TxInfo {
×
1215
        return nil
×
1216
}
×
1217

1218
// ResolutionBlob returns a special opaque blob to be used to sweep/resolve this
1219
// input.
1220
func (bo *breachedOutput) ResolutionBlob() fn.Option[tlv.Blob] {
3✔
1221
        return bo.resolutionBlob
3✔
1222
}
3✔
1223

1224
// Add compile-time constraint ensuring breachedOutput implements the Input
1225
// interface.
1226
var _ input.Input = (*breachedOutput)(nil)
1227

1228
// retributionInfo encapsulates all the data needed to sweep all the contested
1229
// funds within a channel whose contract has been breached by the prior
1230
// counterparty. This struct is used to create the justice transaction which
1231
// spends all outputs of the commitment transaction into an output controlled
1232
// by the wallet.
1233
type retributionInfo struct {
1234
        commitHash   chainhash.Hash
1235
        chanPoint    wire.OutPoint
1236
        chainHash    chainhash.Hash
1237
        breachHeight uint32
1238

1239
        breachedOutputs []breachedOutput
1240
}
1241

1242
// newRetributionInfo constructs a retributionInfo containing all the
1243
// information required by the breach arbiter to recover funds from breached
1244
// channels.  The information is primarily populated using the BreachRetribution
1245
// delivered by the wallet when it detects a channel breach.
1246
func newRetributionInfo(chanPoint *wire.OutPoint,
1247
        breachInfo *lnwallet.BreachRetribution) *retributionInfo {
3✔
1248

3✔
1249
        // Determine the number of second layer HTLCs we will attempt to sweep.
3✔
1250
        nHtlcs := len(breachInfo.HtlcRetributions)
3✔
1251

3✔
1252
        // Initialize a slice to hold the outputs we will attempt to sweep. The
3✔
1253
        // maximum capacity of the slice is set to 2+nHtlcs to handle the case
3✔
1254
        // where the local, remote, and all HTLCs are not dust outputs.  All
3✔
1255
        // HTLC outputs provided by the wallet are guaranteed to be non-dust,
3✔
1256
        // though the commitment outputs are conditionally added depending on
3✔
1257
        // the nil-ness of their sign descriptors.
3✔
1258
        breachedOutputs := make([]breachedOutput, 0, nHtlcs+2)
3✔
1259

3✔
1260
        isTaproot := func() bool {
6✔
1261
                if breachInfo.LocalOutputSignDesc != nil {
6✔
1262
                        return txscript.IsPayToTaproot(
3✔
1263
                                breachInfo.LocalOutputSignDesc.Output.PkScript,
3✔
1264
                        )
3✔
1265
                }
3✔
1266

1267
                return txscript.IsPayToTaproot(
×
1268
                        breachInfo.RemoteOutputSignDesc.Output.PkScript,
×
1269
                )
×
1270
        }()
1271

1272
        // First, record the breach information for the local channel point if
1273
        // it is not considered dust, which is signaled by a non-nil sign
1274
        // descriptor. Here we use CommitmentNoDelay (or
1275
        // CommitmentNoDelayTweakless for newer commitments) since this output
1276
        // belongs to us and has no time-based constraints on spending. For
1277
        // taproot channels, this is a normal spend from our output on the
1278
        // commitment of the remote party.
1279
        if breachInfo.LocalOutputSignDesc != nil {
6✔
1280
                var witnessType input.StandardWitnessType
3✔
1281
                switch {
3✔
1282
                case isTaproot:
3✔
1283
                        witnessType = input.TaprootRemoteCommitSpend
3✔
1284

1285
                case !isTaproot &&
1286
                        breachInfo.LocalOutputSignDesc.SingleTweak == nil:
3✔
1287

3✔
1288
                        witnessType = input.CommitSpendNoDelayTweakless
3✔
1289

1290
                case !isTaproot:
3✔
1291
                        witnessType = input.CommitmentNoDelay
3✔
1292
                }
1293

1294
                // If the local delay is non-zero, it means this output is of
1295
                // the confirmed to_remote type.
1296
                if !isTaproot && breachInfo.LocalDelay != 0 {
6✔
1297
                        witnessType = input.CommitmentToRemoteConfirmed
3✔
1298
                }
3✔
1299

1300
                localOutput := makeBreachedOutput(
3✔
1301
                        &breachInfo.LocalOutpoint,
3✔
1302
                        witnessType,
3✔
1303
                        // No second level script as this is a commitment
3✔
1304
                        // output.
3✔
1305
                        nil,
3✔
1306
                        breachInfo.LocalOutputSignDesc,
3✔
1307
                        breachInfo.BreachHeight,
3✔
1308
                        breachInfo.LocalResolutionBlob,
3✔
1309
                )
3✔
1310

3✔
1311
                breachedOutputs = append(breachedOutputs, localOutput)
3✔
1312
        }
1313

1314
        // Second, record the same information regarding the remote outpoint,
1315
        // again if it is not dust, which belongs to the party who tried to
1316
        // steal our money! Here we set witnessType of the breachedOutput to
1317
        // CommitmentRevoke, since we will be using a revoke key, withdrawing
1318
        // the funds from the commitment transaction immediately.
1319
        if breachInfo.RemoteOutputSignDesc != nil {
6✔
1320
                var witType input.StandardWitnessType
3✔
1321
                if isTaproot {
6✔
1322
                        witType = input.TaprootCommitmentRevoke
3✔
1323
                } else {
6✔
1324
                        witType = input.CommitmentRevoke
3✔
1325
                }
3✔
1326

1327
                remoteOutput := makeBreachedOutput(
3✔
1328
                        &breachInfo.RemoteOutpoint,
3✔
1329
                        witType,
3✔
1330
                        // No second level script as this is a commitment
3✔
1331
                        // output.
3✔
1332
                        nil,
3✔
1333
                        breachInfo.RemoteOutputSignDesc,
3✔
1334
                        breachInfo.BreachHeight,
3✔
1335
                        breachInfo.RemoteResolutionBlob,
3✔
1336
                )
3✔
1337

3✔
1338
                breachedOutputs = append(breachedOutputs, remoteOutput)
3✔
1339
        }
1340

1341
        // Lastly, for each of the breached HTLC outputs, record each as a
1342
        // breached output with the appropriate witness type based on its
1343
        // directionality. All HTLC outputs provided by the wallet are assumed
1344
        // to be non-dust.
1345
        for i, breachedHtlc := range breachInfo.HtlcRetributions {
6✔
1346
                // Using the breachedHtlc's incoming flag, determine the
3✔
1347
                // appropriate witness type that needs to be generated in order
3✔
1348
                // to sweep the HTLC output.
3✔
1349
                var htlcWitnessType input.StandardWitnessType
3✔
1350
                switch {
3✔
1351
                case isTaproot && breachedHtlc.IsIncoming:
3✔
1352
                        htlcWitnessType = input.TaprootHtlcAcceptedRevoke
3✔
1353

1354
                case isTaproot && !breachedHtlc.IsIncoming:
3✔
1355
                        htlcWitnessType = input.TaprootHtlcOfferedRevoke
3✔
1356

1357
                case !isTaproot && breachedHtlc.IsIncoming:
×
1358
                        htlcWitnessType = input.HtlcAcceptedRevoke
×
1359

UNCOV
1360
                case !isTaproot && !breachedHtlc.IsIncoming:
×
UNCOV
1361
                        htlcWitnessType = input.HtlcOfferedRevoke
×
1362
                }
1363

1364
                htlcOutput := makeBreachedOutput(
3✔
1365
                        &breachInfo.HtlcRetributions[i].OutPoint,
3✔
1366
                        htlcWitnessType,
3✔
1367
                        breachInfo.HtlcRetributions[i].SecondLevelWitnessScript,
3✔
1368
                        &breachInfo.HtlcRetributions[i].SignDesc,
3✔
1369
                        breachInfo.BreachHeight,
3✔
1370
                        breachInfo.HtlcRetributions[i].ResolutionBlob,
3✔
1371
                )
3✔
1372

3✔
1373
                // For taproot outputs, we also need to hold onto the second
3✔
1374
                // level tap tweak as well.
3✔
1375
                //nolint:ll
3✔
1376
                htlcOutput.secondLevelTapTweak = breachedHtlc.SecondLevelTapTweak
3✔
1377

3✔
1378
                breachedOutputs = append(breachedOutputs, htlcOutput)
3✔
1379
        }
1380

1381
        return &retributionInfo{
3✔
1382
                commitHash:      breachInfo.BreachTxHash,
3✔
1383
                chainHash:       breachInfo.ChainHash,
3✔
1384
                chanPoint:       *chanPoint,
3✔
1385
                breachedOutputs: breachedOutputs,
3✔
1386
                breachHeight:    breachInfo.BreachHeight,
3✔
1387
        }
3✔
1388
}
1389

1390
// justiceTxVariants is a struct that holds transactions which exacts "justice"
1391
// by sweeping ALL the funds within the channel which we are now entitled to
1392
// due to a breach of the channel's contract by the counterparty. There are
1393
// four variants of justice transactions:
1394
//
1395
// 1. The "normal" justice tx that spends all breached outputs.
1396
// 2. A tx that spends only the breached to_local output and to_remote output
1397
// (can be nil if none of these exist).
1398
// 3. A tx that spends all the breached commitment level HTLC outputs (can be
1399
// nil if none of these exist or if all have been taken to the second level).
1400
// 4. A set of txs that spend all the second-level HTLC outputs (can be empty if
1401
// no HTLC second-level txs have been confirmed).
1402
//
1403
// The reason we create these three variants, is that in certain cases (like
1404
// with the anchor output HTLC malleability), the channel counter party can pin
1405
// the HTLC outputs with low fee children, hindering our normal justice tx that
1406
// attempts to spend these outputs from propagating. In this case we want to
1407
// spend the to_local output and commitment level HTLC outputs separately,
1408
// before the CSV locks expire.
1409
type justiceTxVariants struct {
1410
        spendAll              *justiceTxCtx
1411
        spendCommitOuts       *justiceTxCtx
1412
        spendHTLCs            *justiceTxCtx
1413
        spendSecondLevelHTLCs []*justiceTxCtx
1414
}
1415

1416
// createJusticeTx creates transactions which exacts "justice" by sweeping ALL
1417
// the funds within the channel which we are now entitled to due to a breach of
1418
// the channel's contract by the counterparty. This function returns a *fully*
1419
// signed transaction with the witness for each input fully in place.
1420
func (b *BreachArbitrator) createJusticeTx(
1421
        breachedOutputs []breachedOutput) (*justiceTxVariants, error) {
3✔
1422

3✔
1423
        var (
3✔
1424
                allInputs         []input.Input
3✔
1425
                commitInputs      []input.Input
3✔
1426
                htlcInputs        []input.Input
3✔
1427
                secondLevelInputs []input.Input
3✔
1428
        )
3✔
1429

3✔
1430
        for i := range breachedOutputs {
6✔
1431
                // Grab locally scoped reference to breached output.
3✔
1432
                inp := &breachedOutputs[i]
3✔
1433
                allInputs = append(allInputs, inp)
3✔
1434

3✔
1435
                // Check if the input is from a commitment output, a commitment
3✔
1436
                // level HTLC output or a second level HTLC output.
3✔
1437
                switch inp.WitnessType() {
3✔
1438
                case input.HtlcAcceptedRevoke, input.HtlcOfferedRevoke,
1439
                        input.TaprootHtlcAcceptedRevoke,
1440
                        input.TaprootHtlcOfferedRevoke:
3✔
1441

3✔
1442
                        htlcInputs = append(htlcInputs, inp)
3✔
1443

1444
                case input.HtlcSecondLevelRevoke,
UNCOV
1445
                        input.TaprootHtlcSecondLevelRevoke:
×
UNCOV
1446

×
UNCOV
1447
                        secondLevelInputs = append(secondLevelInputs, inp)
×
1448

1449
                default:
3✔
1450
                        commitInputs = append(commitInputs, inp)
3✔
1451
                }
1452
        }
1453

1454
        var (
3✔
1455
                txs = &justiceTxVariants{}
3✔
1456
                err error
3✔
1457
        )
3✔
1458

3✔
1459
        // For each group of inputs, create a tx that spends them.
3✔
1460
        txs.spendAll, err = b.createSweepTx(allInputs...)
3✔
1461
        if err != nil {
3✔
1462
                return nil, err
×
1463
        }
×
1464

1465
        txs.spendCommitOuts, err = b.createSweepTx(commitInputs...)
3✔
1466
        if err != nil {
3✔
1467
                brarLog.Errorf("could not create sweep tx for commitment "+
×
1468
                        "outputs: %v", err)
×
1469
        }
×
1470

1471
        txs.spendHTLCs, err = b.createSweepTx(htlcInputs...)
3✔
1472
        if err != nil {
3✔
1473
                brarLog.Errorf("could not create sweep tx for HTLC outputs: %v",
×
1474
                        err)
×
1475
        }
×
1476

1477
        // TODO(roasbeef): only register one of them?
1478

1479
        secondLevelSweeps := make([]*justiceTxCtx, 0, len(secondLevelInputs))
3✔
1480
        for _, input := range secondLevelInputs {
3✔
UNCOV
1481
                sweepTx, err := b.createSweepTx(input)
×
UNCOV
1482
                if err != nil {
×
1483
                        brarLog.Errorf("could not create sweep tx for "+
×
1484
                                "second-level HTLC output: %v", err)
×
1485

×
1486
                        continue
×
1487
                }
1488

UNCOV
1489
                secondLevelSweeps = append(secondLevelSweeps, sweepTx)
×
1490
        }
1491
        txs.spendSecondLevelHTLCs = secondLevelSweeps
3✔
1492

3✔
1493
        return txs, nil
3✔
1494
}
1495

1496
// justiceTxCtx contains the justice transaction along with other related meta
1497
// data.
1498
type justiceTxCtx struct {
1499
        justiceTx *wire.MsgTx
1500

1501
        sweepAddr lnwallet.AddrWithKey
1502

1503
        extraTxOut fn.Option[sweep.SweepOutput]
1504

1505
        fee btcutil.Amount
1506

1507
        inputs []input.Input
1508
}
1509

1510
// createSweepTx creates a tx that sweeps the passed inputs back to our wallet.
1511
func (b *BreachArbitrator) createSweepTx(
1512
        inputs ...input.Input) (*justiceTxCtx, error) {
3✔
1513

3✔
1514
        if len(inputs) == 0 {
6✔
1515
                return nil, nil
3✔
1516
        }
3✔
1517

1518
        // We will assemble the breached outputs into a slice of spendable
1519
        // outputs, while simultaneously computing the estimated weight of the
1520
        // transaction.
1521
        var (
3✔
1522
                spendableOutputs []input.Input
3✔
1523
                weightEstimate   input.TxWeightEstimator
3✔
1524
        )
3✔
1525

3✔
1526
        // Allocate enough space to potentially hold each of the breached
3✔
1527
        // outputs in the retribution info.
3✔
1528
        spendableOutputs = make([]input.Input, 0, len(inputs))
3✔
1529

3✔
1530
        // The justice transaction we construct will be a segwit transaction
3✔
1531
        // that pays to a p2tr output. Components such as the version,
3✔
1532
        // nLockTime, and output are already included in the TxWeightEstimator.
3✔
1533
        weightEstimate.AddP2TROutput()
3✔
1534

3✔
1535
        // If any of our inputs has a resolution blob, then we'll add another
3✔
1536
        // P2TR _output_, since we'll want to separate the custom channel
3✔
1537
        // outputs from the regular, BTC only outputs. So we only need one such
3✔
1538
        // output, which'll carry the custom channel "valuables" from both the
3✔
1539
        // breached commitment and HTLC outputs.
3✔
1540
        hasBlobs := fn.Any(inputs, func(i input.Input) bool {
6✔
1541
                return i.ResolutionBlob().IsSome()
3✔
1542
        })
3✔
1543
        if hasBlobs {
6✔
1544
                weightEstimate.AddP2TROutput()
3✔
1545
        }
3✔
1546

1547
        // Next, we iterate over the breached outputs contained in the
1548
        // retribution info.  For each, we switch over the witness type such
1549
        // that we contribute the appropriate weight for each input and
1550
        // witness, finally adding to our list of spendable outputs.
1551
        for i := range inputs {
6✔
1552
                // Grab locally scoped reference to breached output.
3✔
1553
                inp := inputs[i]
3✔
1554

3✔
1555
                // First, determine the appropriate estimated witness weight
3✔
1556
                // for the give witness type of this breached output. If the
3✔
1557
                // witness weight cannot be estimated, we will omit it from the
3✔
1558
                // transaction.
3✔
1559
                witnessWeight, _, err := inp.WitnessType().SizeUpperBound()
3✔
1560
                if err != nil {
3✔
1561
                        brarLog.Warnf("could not determine witness weight "+
×
1562
                                "for breached output in retribution info: %v",
×
1563
                                err)
×
1564
                        continue
×
1565
                }
1566
                weightEstimate.AddWitnessInput(witnessWeight)
3✔
1567

3✔
1568
                // Finally, append this input to our list of spendable outputs.
3✔
1569
                spendableOutputs = append(spendableOutputs, inp)
3✔
1570
        }
1571

1572
        txWeight := weightEstimate.Weight()
3✔
1573

3✔
1574
        return b.sweepSpendableOutputsTxn(txWeight, spendableOutputs...)
3✔
1575
}
1576

1577
// sweepSpendableOutputsTxn creates a signed transaction from a sequence of
1578
// spendable outputs by sweeping the funds into a single p2wkh output.
1579
func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
1580
        inputs ...input.Input) (*justiceTxCtx, error) {
3✔
1581

3✔
1582
        // First, we obtain a new public key script from the wallet which we'll
3✔
1583
        // sweep the funds to.
3✔
1584
        // TODO(roasbeef): possibly create many outputs to minimize change in
3✔
1585
        // the future?
3✔
1586
        pkScript, err := b.cfg.GenSweepScript().Unpack()
3✔
1587
        if err != nil {
3✔
1588
                return nil, err
×
1589
        }
×
1590

1591
        // Compute the total amount contained in the inputs.
1592
        var totalAmt btcutil.Amount
3✔
1593
        for _, inp := range inputs {
6✔
1594
                totalAmt += btcutil.Amount(inp.SignDesc().Output.Value)
3✔
1595
        }
3✔
1596

1597
        // We'll actually attempt to target inclusion within the next two
1598
        // blocks as we'd like to sweep these funds back into our wallet ASAP.
1599
        feePerKw, err := b.cfg.Estimator.EstimateFeePerKW(justiceTxConfTarget)
3✔
1600
        if err != nil {
3✔
1601
                return nil, err
×
1602
        }
×
1603
        txFee := feePerKw.FeeForWeight(txWeight)
3✔
1604

3✔
1605
        // At this point, we'll check to see if we have any extra outputs to
3✔
1606
        // add from the aux sweeper.
3✔
1607
        extraChangeOut := fn.MapOptionZ(
3✔
1608
                b.cfg.AuxSweeper,
3✔
1609
                func(aux sweep.AuxSweeper) fn.Result[sweep.SweepOutput] {
3✔
1610
                        return aux.DeriveSweepAddr(inputs, pkScript)
×
1611
                },
×
1612
        )
1613
        if err := extraChangeOut.Err(); err != nil {
3✔
1614
                return nil, err
×
1615
        }
×
1616

1617
        // TODO(roasbeef): already start to siphon their funds into fees
1618
        sweepAmt := int64(totalAmt - txFee)
3✔
1619

3✔
1620
        // With the fee calculated, we can now create the transaction using the
3✔
1621
        // information gathered above and the provided retribution information.
3✔
1622
        txn := wire.NewMsgTx(2)
3✔
1623

3✔
1624
        // First, we'll add the extra sweep output if it exists, subtracting the
3✔
1625
        // amount from the sweep amt.
3✔
1626
        if b.cfg.AuxSweeper.IsSome() {
3✔
1627
                extraChangeOut.WhenOk(func(o sweep.SweepOutput) {
×
1628
                        sweepAmt -= o.Value
×
1629

×
1630
                        txn.AddTxOut(&o.TxOut)
×
1631
                })
×
1632
        }
1633

1634
        // Next, we'll add the output to which our funds will be deposited.
1635
        txn.AddTxOut(&wire.TxOut{
3✔
1636
                PkScript: pkScript.DeliveryAddress,
3✔
1637
                Value:    sweepAmt,
3✔
1638
        })
3✔
1639

3✔
1640
        // TODO(roasbeef): add other output change modify sweep amt
3✔
1641

3✔
1642
        // Next, we add all of the spendable outputs as inputs to the
3✔
1643
        // transaction.
3✔
1644
        for _, inp := range inputs {
6✔
1645
                txn.AddTxIn(&wire.TxIn{
3✔
1646
                        PreviousOutPoint: inp.OutPoint(),
3✔
1647
                        Sequence:         inp.BlocksToMaturity(),
3✔
1648
                })
3✔
1649
        }
3✔
1650

1651
        // Before signing the transaction, check to ensure that it meets some
1652
        // basic validity requirements.
1653
        btx := btcutil.NewTx(txn)
3✔
1654
        if err := blockchain.CheckTransactionSanity(btx); err != nil {
3✔
1655
                return nil, err
×
1656
        }
×
1657

1658
        // Create a sighash cache to improve the performance of hashing and
1659
        // signing SigHashAll inputs.
1660
        prevOutputFetcher, err := input.MultiPrevOutFetcher(inputs)
3✔
1661
        if err != nil {
3✔
1662
                return nil, err
×
1663
        }
×
1664
        hashCache := txscript.NewTxSigHashes(txn, prevOutputFetcher)
3✔
1665

3✔
1666
        // Create a closure that encapsulates the process of initializing a
3✔
1667
        // particular output's witness generation function, computing the
3✔
1668
        // witness, and attaching it to the transaction. This function accepts
3✔
1669
        // an integer index representing the intended txin index, and the
3✔
1670
        // breached output from which it will spend.
3✔
1671
        addWitness := func(idx int, so input.Input) error {
6✔
1672
                // First, we construct a valid witness for this outpoint and
3✔
1673
                // transaction using the SpendableOutput's witness generation
3✔
1674
                // function.
3✔
1675
                inputScript, err := so.CraftInputScript(
3✔
1676
                        b.cfg.Signer, txn, hashCache, prevOutputFetcher, idx,
3✔
1677
                )
3✔
1678
                if err != nil {
3✔
1679
                        return err
×
1680
                }
×
1681

1682
                // Then, we add the witness to the transaction at the
1683
                // appropriate txin index.
1684
                txn.TxIn[idx].Witness = inputScript.Witness
3✔
1685

3✔
1686
                return nil
3✔
1687
        }
1688

1689
        // Finally, generate a witness for each output and attach it to the
1690
        // transaction.
1691
        for i, inp := range inputs {
6✔
1692
                if err := addWitness(i, inp); err != nil {
3✔
1693
                        return nil, err
×
1694
                }
×
1695
        }
1696

1697
        return &justiceTxCtx{
3✔
1698
                justiceTx:  txn,
3✔
1699
                sweepAddr:  pkScript,
3✔
1700
                extraTxOut: extraChangeOut.OkToSome(),
3✔
1701
                fee:        txFee,
3✔
1702
                inputs:     inputs,
3✔
1703
        }, nil
3✔
1704
}
1705

1706
// RetributionStore handles persistence of retribution states to disk and is
1707
// backed by a boltdb bucket. The primary responsibility of the retribution
1708
// store is to ensure that we can recover from a restart in the middle of a
1709
// breached contract retribution.
1710
type RetributionStore struct {
1711
        db kvdb.Backend
1712
}
1713

1714
// NewRetributionStore creates a new instance of a RetributionStore.
1715
func NewRetributionStore(db kvdb.Backend) *RetributionStore {
3✔
1716
        return &RetributionStore{
3✔
1717
                db: db,
3✔
1718
        }
3✔
1719
}
3✔
1720

1721
// taprootBriefcaseFromRetInfo creates a taprootBriefcase from a retribution
1722
// info struct. This stores all the tap tweak information we need to inrder to
1723
// be able to hadnel breaches after a restart.
1724
func taprootBriefcaseFromRetInfo(retInfo *retributionInfo) *taprootBriefcase {
3✔
1725
        tapCase := newTaprootBriefcase()
3✔
1726

3✔
1727
        for _, bo := range retInfo.breachedOutputs {
6✔
1728
                switch bo.WitnessType() {
3✔
1729
                // For spending from our commitment output on the remote
1730
                // commitment, we'll need to stash the control block.
1731
                case input.TaprootRemoteCommitSpend:
3✔
1732
                        //nolint:ll
3✔
1733
                        tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock = bo.signDesc.ControlBlock
3✔
1734

3✔
1735
                        bo.resolutionBlob.WhenSome(func(blob tlv.Blob) {
6✔
1736
                                tapCase.SettledCommitBlob = tlv.SomeRecordT(
3✔
1737
                                        tlv.NewPrimitiveRecord[tlv.TlvType2](
3✔
1738
                                                blob,
3✔
1739
                                        ),
3✔
1740
                                )
3✔
1741
                        })
3✔
1742

1743
                // To spend the revoked output again, we'll store the same
1744
                // control block value as above, but in a different place.
1745
                case input.TaprootCommitmentRevoke:
3✔
1746
                        //nolint:ll
3✔
1747
                        tapCase.CtrlBlocks.Val.RevokeSweepCtrlBlock = bo.signDesc.ControlBlock
3✔
1748

3✔
1749
                        bo.resolutionBlob.WhenSome(func(blob tlv.Blob) {
6✔
1750
                                tapCase.BreachedCommitBlob = tlv.SomeRecordT(
3✔
1751
                                        tlv.NewPrimitiveRecord[tlv.TlvType3](
3✔
1752
                                                blob,
3✔
1753
                                        ),
3✔
1754
                                )
3✔
1755
                        })
3✔
1756

1757
                // For spending the HTLC outputs, we'll store the first and
1758
                // second level tweak values.
1759
                case input.TaprootHtlcAcceptedRevoke:
3✔
1760
                        fallthrough
3✔
1761
                case input.TaprootHtlcOfferedRevoke:
3✔
1762
                        resID := newResolverID(bo.OutPoint())
3✔
1763

3✔
1764
                        var firstLevelTweak [32]byte
3✔
1765
                        copy(firstLevelTweak[:], bo.signDesc.TapTweak)
3✔
1766
                        secondLevelTweak := bo.secondLevelTapTweak
3✔
1767

3✔
1768
                        //nolint:ll
3✔
1769
                        tapCase.TapTweaks.Val.BreachedHtlcTweaks[resID] = firstLevelTweak
3✔
1770

3✔
1771
                        //nolint:ll
3✔
1772
                        tapCase.TapTweaks.Val.BreachedSecondLevelHltcTweaks[resID] = secondLevelTweak
3✔
1773
                }
1774
        }
1775

1776
        return tapCase
3✔
1777
}
1778

1779
// applyTaprootRetInfo attaches the taproot specific information in the tapCase
1780
// to the passed retInfo struct.
1781
func applyTaprootRetInfo(tapCase *taprootBriefcase,
1782
        retInfo *retributionInfo) error {
3✔
1783

3✔
1784
        for i := range retInfo.breachedOutputs {
6✔
1785
                bo := retInfo.breachedOutputs[i]
3✔
1786

3✔
1787
                switch bo.WitnessType() {
3✔
1788
                // For spending from our commitment output on the remote
1789
                // commitment, we'll apply the control block.
1790
                case input.TaprootRemoteCommitSpend:
3✔
1791
                        //nolint:ll
3✔
1792
                        bo.signDesc.ControlBlock = tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock
3✔
1793

3✔
1794
                        tapCase.SettledCommitBlob.WhenSomeV(
3✔
1795
                                func(blob tlv.Blob) {
6✔
1796
                                        bo.resolutionBlob = fn.Some(blob)
3✔
1797
                                },
3✔
1798
                        )
1799

1800
                // To spend the revoked output again, we'll apply the same
1801
                // control block value as above, but to a different place.
1802
                case input.TaprootCommitmentRevoke:
3✔
1803
                        //nolint:ll
3✔
1804
                        bo.signDesc.ControlBlock = tapCase.CtrlBlocks.Val.RevokeSweepCtrlBlock
3✔
1805

3✔
1806
                        tapCase.BreachedCommitBlob.WhenSomeV(
3✔
1807
                                func(blob tlv.Blob) {
6✔
1808
                                        bo.resolutionBlob = fn.Some(blob)
3✔
1809
                                },
3✔
1810
                        )
1811

1812
                // For spending the HTLC outputs, we'll apply the first and
1813
                // second level tweak values.
1814
                case input.TaprootHtlcAcceptedRevoke:
3✔
1815
                        fallthrough
3✔
1816
                case input.TaprootHtlcOfferedRevoke:
3✔
1817
                        resID := newResolverID(bo.OutPoint())
3✔
1818

3✔
1819
                        //nolint:ll
3✔
1820
                        tap1, ok := tapCase.TapTweaks.Val.BreachedHtlcTweaks[resID]
3✔
1821
                        if !ok {
3✔
1822
                                return fmt.Errorf("unable to find taproot "+
×
1823
                                        "tweak for: %v", bo.OutPoint())
×
1824
                        }
×
1825
                        bo.signDesc.TapTweak = tap1[:]
3✔
1826

3✔
1827
                        //nolint:ll
3✔
1828
                        tap2, ok := tapCase.TapTweaks.Val.BreachedSecondLevelHltcTweaks[resID]
3✔
1829
                        if !ok {
3✔
1830
                                return fmt.Errorf("unable to find taproot "+
×
1831
                                        "tweak for: %v", bo.OutPoint())
×
1832
                        }
×
1833
                        bo.secondLevelTapTweak = tap2
3✔
1834
                }
1835

1836
                retInfo.breachedOutputs[i] = bo
3✔
1837
        }
1838

1839
        return nil
3✔
1840
}
1841

1842
// Add adds a retribution state to the RetributionStore, which is then persisted
1843
// to disk.
1844
func (rs *RetributionStore) Add(ret *retributionInfo) error {
3✔
1845
        return kvdb.Update(rs.db, func(tx kvdb.RwTx) error {
6✔
1846
                // If this is our first contract breach, the retributionBucket
3✔
1847
                // won't exist, in which case, we just create a new bucket.
3✔
1848
                retBucket, err := tx.CreateTopLevelBucket(retributionBucket)
3✔
1849
                if err != nil {
3✔
1850
                        return err
×
1851
                }
×
1852
                tapRetBucket, err := tx.CreateTopLevelBucket(
3✔
1853
                        taprootRetributionBucket,
3✔
1854
                )
3✔
1855
                if err != nil {
3✔
1856
                        return err
×
1857
                }
×
1858

1859
                var outBuf bytes.Buffer
3✔
1860
                err = graphdb.WriteOutpoint(&outBuf, &ret.chanPoint)
3✔
1861
                if err != nil {
3✔
1862
                        return err
×
1863
                }
×
1864

1865
                var retBuf bytes.Buffer
3✔
1866
                if err := ret.Encode(&retBuf); err != nil {
3✔
1867
                        return err
×
1868
                }
×
1869

1870
                err = retBucket.Put(outBuf.Bytes(), retBuf.Bytes())
3✔
1871
                if err != nil {
3✔
1872
                        return err
×
1873
                }
×
1874

1875
                // If this isn't a taproot channel, then we can exit early here
1876
                // as there's no extra data to write.
1877
                switch {
3✔
1878
                case len(ret.breachedOutputs) == 0:
×
1879
                        return nil
×
1880

1881
                case !txscript.IsPayToTaproot(
1882
                        ret.breachedOutputs[0].signDesc.Output.PkScript,
1883
                ):
3✔
1884
                        return nil
3✔
1885
                }
1886

1887
                // We'll also map the ret info into the taproot storage
1888
                // structure we need for taproot channels.
1889
                var b bytes.Buffer
3✔
1890
                tapRetcase := taprootBriefcaseFromRetInfo(ret)
3✔
1891
                if err := tapRetcase.Encode(&b); err != nil {
3✔
1892
                        return err
×
1893
                }
×
1894

1895
                return tapRetBucket.Put(outBuf.Bytes(), b.Bytes())
3✔
1896
        }, func() {})
3✔
1897
}
1898

1899
// IsBreached queries the retribution store to discern if this channel was
1900
// previously breached. This is used when connecting to a peer to determine if
1901
// it is safe to add a link to the htlcswitch, as we should never add a channel
1902
// that has already been breached.
1903
func (rs *RetributionStore) IsBreached(chanPoint *wire.OutPoint) (bool, error) {
3✔
1904
        var found bool
3✔
1905
        err := kvdb.View(rs.db, func(tx kvdb.RTx) error {
6✔
1906
                retBucket := tx.ReadBucket(retributionBucket)
3✔
1907
                if retBucket == nil {
6✔
1908
                        return nil
3✔
1909
                }
3✔
1910

1911
                var chanBuf bytes.Buffer
3✔
1912
                err := graphdb.WriteOutpoint(&chanBuf, chanPoint)
3✔
1913
                if err != nil {
3✔
1914
                        return err
×
1915
                }
×
1916

1917
                retInfo := retBucket.Get(chanBuf.Bytes())
3✔
1918
                if retInfo != nil {
6✔
1919
                        found = true
3✔
1920
                }
3✔
1921

1922
                return nil
3✔
1923
        }, func() {
3✔
1924
                found = false
3✔
1925
        })
3✔
1926

1927
        return found, err
3✔
1928
}
1929

1930
// Remove removes a retribution state and finalized justice transaction by
1931
// channel point  from the retribution store.
1932
func (rs *RetributionStore) Remove(chanPoint *wire.OutPoint) error {
3✔
1933
        return kvdb.Update(rs.db, func(tx kvdb.RwTx) error {
6✔
1934
                retBucket := tx.ReadWriteBucket(retributionBucket)
3✔
1935
                tapRetBucket, err := tx.CreateTopLevelBucket(
3✔
1936
                        taprootRetributionBucket,
3✔
1937
                )
3✔
1938
                if err != nil {
3✔
1939
                        return err
×
1940
                }
×
1941

1942
                // We return an error if the bucket is not already created,
1943
                // since normal operation of the breach arbiter should never
1944
                // try to remove a finalized retribution state that is not
1945
                // already stored in the db.
1946
                if retBucket == nil {
3✔
UNCOV
1947
                        return errors.New("unable to remove retribution " +
×
UNCOV
1948
                                "because the retribution bucket doesn't exist")
×
UNCOV
1949
                }
×
1950

1951
                // Serialize the channel point we are intending to remove.
1952
                var chanBuf bytes.Buffer
3✔
1953
                err = graphdb.WriteOutpoint(&chanBuf, chanPoint)
3✔
1954
                if err != nil {
3✔
1955
                        return err
×
1956
                }
×
1957
                chanBytes := chanBuf.Bytes()
3✔
1958

3✔
1959
                // Remove the persisted retribution info and finalized justice
3✔
1960
                // transaction.
3✔
1961
                if err := retBucket.Delete(chanBytes); err != nil {
3✔
1962
                        return err
×
1963
                }
×
1964

1965
                return tapRetBucket.Delete(chanBytes)
3✔
1966
        }, func() {})
3✔
1967
}
1968

1969
// ForAll iterates through all stored retributions and executes the passed
1970
// callback function on each retribution.
1971
func (rs *RetributionStore) ForAll(cb func(*retributionInfo) error,
1972
        reset func()) error {
3✔
1973

3✔
1974
        return kvdb.View(rs.db, func(tx kvdb.RTx) error {
6✔
1975
                // If the bucket does not exist, then there are no pending
3✔
1976
                // retributions.
3✔
1977
                retBucket := tx.ReadBucket(retributionBucket)
3✔
1978
                if retBucket == nil {
6✔
1979
                        return nil
3✔
1980
                }
3✔
1981
                tapRetBucket := tx.ReadBucket(
3✔
1982
                        taprootRetributionBucket,
3✔
1983
                )
3✔
1984

3✔
1985
                // Otherwise, we fetch each serialized retribution info,
3✔
1986
                // deserialize it, and execute the passed in callback function
3✔
1987
                // on it.
3✔
1988
                return retBucket.ForEach(func(k, retBytes []byte) error {
6✔
1989
                        ret := &retributionInfo{}
3✔
1990
                        err := ret.Decode(bytes.NewBuffer(retBytes))
3✔
1991
                        if err != nil {
3✔
1992
                                return err
×
1993
                        }
×
1994

1995
                        tapInfoBytes := tapRetBucket.Get(k)
3✔
1996
                        if tapInfoBytes != nil {
6✔
1997
                                var tapCase taprootBriefcase
3✔
1998
                                err := tapCase.Decode(
3✔
1999
                                        bytes.NewReader(tapInfoBytes),
3✔
2000
                                )
3✔
2001
                                if err != nil {
3✔
2002
                                        return err
×
2003
                                }
×
2004

2005
                                err = applyTaprootRetInfo(&tapCase, ret)
3✔
2006
                                if err != nil {
3✔
2007
                                        return err
×
2008
                                }
×
2009
                        }
2010

2011
                        return cb(ret)
3✔
2012
                })
2013
        }, reset)
2014
}
2015

2016
// Encode serializes the retribution into the passed byte stream.
2017
func (ret *retributionInfo) Encode(w io.Writer) error {
3✔
2018
        var scratch [4]byte
3✔
2019

3✔
2020
        if _, err := w.Write(ret.commitHash[:]); err != nil {
3✔
2021
                return err
×
2022
        }
×
2023

2024
        if err := graphdb.WriteOutpoint(w, &ret.chanPoint); err != nil {
3✔
2025
                return err
×
2026
        }
×
2027

2028
        if _, err := w.Write(ret.chainHash[:]); err != nil {
3✔
2029
                return err
×
2030
        }
×
2031

2032
        binary.BigEndian.PutUint32(scratch[:], ret.breachHeight)
3✔
2033
        if _, err := w.Write(scratch[:]); err != nil {
3✔
2034
                return err
×
2035
        }
×
2036

2037
        nOutputs := len(ret.breachedOutputs)
3✔
2038
        if err := wire.WriteVarInt(w, 0, uint64(nOutputs)); err != nil {
3✔
2039
                return err
×
2040
        }
×
2041

2042
        for _, output := range ret.breachedOutputs {
6✔
2043
                if err := output.Encode(w); err != nil {
3✔
2044
                        return err
×
2045
                }
×
2046
        }
2047

2048
        return nil
3✔
2049
}
2050

2051
// Decode deserializes a retribution from the passed byte stream.
2052
func (ret *retributionInfo) Decode(r io.Reader) error {
3✔
2053
        var scratch [32]byte
3✔
2054

3✔
2055
        if _, err := io.ReadFull(r, scratch[:]); err != nil {
3✔
2056
                return err
×
2057
        }
×
2058
        hash, err := chainhash.NewHash(scratch[:])
3✔
2059
        if err != nil {
3✔
2060
                return err
×
2061
        }
×
2062
        ret.commitHash = *hash
3✔
2063

3✔
2064
        if err := graphdb.ReadOutpoint(r, &ret.chanPoint); err != nil {
3✔
2065
                return err
×
2066
        }
×
2067

2068
        if _, err := io.ReadFull(r, scratch[:]); err != nil {
3✔
2069
                return err
×
2070
        }
×
2071
        chainHash, err := chainhash.NewHash(scratch[:])
3✔
2072
        if err != nil {
3✔
2073
                return err
×
2074
        }
×
2075
        ret.chainHash = *chainHash
3✔
2076

3✔
2077
        if _, err := io.ReadFull(r, scratch[:4]); err != nil {
3✔
2078
                return err
×
2079
        }
×
2080
        ret.breachHeight = binary.BigEndian.Uint32(scratch[:4])
3✔
2081

3✔
2082
        nOutputsU64, err := wire.ReadVarInt(r, 0)
3✔
2083
        if err != nil {
3✔
2084
                return err
×
2085
        }
×
2086
        nOutputs := int(nOutputsU64)
3✔
2087

3✔
2088
        ret.breachedOutputs = make([]breachedOutput, nOutputs)
3✔
2089
        for i := range ret.breachedOutputs {
6✔
2090
                if err := ret.breachedOutputs[i].Decode(r); err != nil {
3✔
2091
                        return err
×
2092
                }
×
2093
        }
2094

2095
        return nil
3✔
2096
}
2097

2098
// Encode serializes a breachedOutput into the passed byte stream.
2099
func (bo *breachedOutput) Encode(w io.Writer) error {
3✔
2100
        var scratch [8]byte
3✔
2101

3✔
2102
        binary.BigEndian.PutUint64(scratch[:8], uint64(bo.amt))
3✔
2103
        if _, err := w.Write(scratch[:8]); err != nil {
3✔
2104
                return err
×
2105
        }
×
2106

2107
        if err := graphdb.WriteOutpoint(w, &bo.outpoint); err != nil {
3✔
2108
                return err
×
2109
        }
×
2110

2111
        err := input.WriteSignDescriptor(w, &bo.signDesc)
3✔
2112
        if err != nil {
3✔
2113
                return err
×
2114
        }
×
2115

2116
        err = wire.WriteVarBytes(w, 0, bo.secondLevelWitnessScript)
3✔
2117
        if err != nil {
3✔
2118
                return err
×
2119
        }
×
2120

2121
        binary.BigEndian.PutUint16(scratch[:2], uint16(bo.witnessType))
3✔
2122
        if _, err := w.Write(scratch[:2]); err != nil {
3✔
2123
                return err
×
2124
        }
×
2125

2126
        return nil
3✔
2127
}
2128

2129
// Decode deserializes a breachedOutput from the passed byte stream.
2130
func (bo *breachedOutput) Decode(r io.Reader) error {
3✔
2131
        var scratch [8]byte
3✔
2132

3✔
2133
        if _, err := io.ReadFull(r, scratch[:8]); err != nil {
3✔
2134
                return err
×
2135
        }
×
2136
        bo.amt = btcutil.Amount(binary.BigEndian.Uint64(scratch[:8]))
3✔
2137

3✔
2138
        if err := graphdb.ReadOutpoint(r, &bo.outpoint); err != nil {
3✔
2139
                return err
×
2140
        }
×
2141

2142
        if err := input.ReadSignDescriptor(r, &bo.signDesc); err != nil {
3✔
2143
                return err
×
2144
        }
×
2145

2146
        wScript, err := wire.ReadVarBytes(r, 0, 1000, "witness script")
3✔
2147
        if err != nil {
3✔
2148
                return err
×
2149
        }
×
2150
        bo.secondLevelWitnessScript = wScript
3✔
2151

3✔
2152
        if _, err := io.ReadFull(r, scratch[:2]); err != nil {
3✔
2153
                return err
×
2154
        }
×
2155
        bo.witnessType = input.StandardWitnessType(
3✔
2156
                binary.BigEndian.Uint16(scratch[:2]),
3✔
2157
        )
3✔
2158

3✔
2159
        return nil
3✔
2160
}
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