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

lightningnetwork / lnd / 13211764208

08 Feb 2025 03:08AM UTC coverage: 49.288% (-9.5%) from 58.815%
13211764208

Pull #9489

github

calvinrzachman
itest: verify switchrpc server enforces send then track

We prevent the rpc server from allowing onion dispatches for
attempt IDs which have already been tracked by rpc clients.

This helps protect the client from leaking a duplicate onion
attempt. NOTE: This is not the only method for solving this
issue! The issue could be addressed via careful client side
programming which accounts for the uncertainty and async
nature of dispatching onions to a remote process via RPC.
This would require some lnd ChannelRouter changes for how
we intend to use these RPCs though.
Pull Request #9489: multi: add BuildOnion, SendOnion, and TrackOnion RPCs

474 of 990 new or added lines in 11 files covered. (47.88%)

27321 existing lines in 435 files now uncovered.

101192 of 205306 relevant lines covered (49.29%)

1.54 hits per line

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

41.62
/contractcourt/utxonursery.go
1
package contractcourt
2

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

12
        "github.com/btcsuite/btcd/btcutil"
13
        "github.com/btcsuite/btcd/txscript"
14
        "github.com/btcsuite/btcd/wire"
15
        "github.com/davecgh/go-spew/spew"
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/labels"
22
        "github.com/lightningnetwork/lnd/lnutils"
23
        "github.com/lightningnetwork/lnd/lnwallet"
24
        "github.com/lightningnetwork/lnd/sweep"
25
        "github.com/lightningnetwork/lnd/tlv"
26
)
27

28
//                          SUMMARY OF OUTPUT STATES
29
//
30
//  - CRIB
31
//    - SerializedType: babyOutput
32
//    - OriginalOutputType: HTLC
33
//    - Awaiting: First-stage HTLC CLTV expiry
34
//    - HeightIndexEntry: Absolute block height of CLTV expiry.
35
//    - NextState: KNDR
36
//  - PSCL
37
//    - SerializedType: kidOutput
38
//    - OriginalOutputType: Commitment
39
//    - Awaiting: Confirmation of commitment txn
40
//    - HeightIndexEntry: None.
41
//    - NextState: KNDR
42
//  - KNDR
43
//    - SerializedType: kidOutput
44
//    - OriginalOutputType: Commitment or HTLC
45
//    - Awaiting: Commitment CSV expiry or second-stage HTLC CSV expiry.
46
//    - HeightIndexEntry: Input confirmation height + relative CSV delay
47
//    - NextState: GRAD
48
//  - GRAD:
49
//    - SerializedType: kidOutput
50
//    - OriginalOutputType: Commitment or HTLC
51
//    - Awaiting: All other outputs in channel to become GRAD.
52
//    - NextState: Mark channel fully closed in channeldb and remove.
53
//
54
//                        DESCRIPTION OF OUTPUT STATES
55
//
56
// TODO(roasbeef): update comment with both new output types
57
//
58
//  - CRIB (babyOutput) outputs are two-stage htlc outputs that are initially
59
//    locked using a CLTV delay, followed by a CSV delay. The first stage of a
60
//    crib output requires broadcasting a presigned htlc timeout txn generated
61
//    by the wallet after an absolute expiry height. Since the timeout txns are
62
//    predetermined, they cannot be batched after-the-fact, meaning that all
63
//    CRIB outputs are broadcast and confirmed independently. After the first
64
//    stage is complete, a CRIB output is moved to the KNDR state, which will
65
//    finishing sweeping the second-layer CSV delay.
66
//
67
//  - PSCL (kidOutput) outputs are commitment outputs locked under a CSV delay.
68
//    These outputs are stored temporarily in this state until the commitment
69
//    transaction confirms, as this solidifies an absolute height that the
70
//    relative time lock will expire. Once this maturity height is determined,
71
//    the PSCL output is moved into KNDR.
72
//
73
//  - KNDR (kidOutput) outputs are CSV delayed outputs for which the maturity
74
//    height has been fully determined. This results from having received
75
//    confirmation of the UTXO we are trying to spend, contained in either the
76
//    commitment txn or htlc timeout txn. Once the maturity height is reached,
77
//    the utxo nursery will sweep all KNDR outputs scheduled for that height
78
//    using a single txn.
79
//
80
//  - GRAD (kidOutput) outputs are KNDR outputs that have successfully been
81
//    swept into the user's wallet. A channel is considered mature once all of
82
//    its outputs, including two-stage htlcs, have entered the GRAD state,
83
//    indicating that it safe to mark the channel as fully closed.
84
//
85
//
86
//                     OUTPUT STATE TRANSITIONS IN UTXO NURSERY
87
//
88
//      ┌────────────────┐            ┌──────────────┐
89
//      │ Commit Outputs │            │ HTLC Outputs │
90
//      └────────────────┘            └──────────────┘
91
//               │                            │
92
//               │                            │
93
//               │                            │               UTXO NURSERY
94
//   ┌───────────┼────────────────┬───────────┼───────────────────────────────┐
95
//   │           │                            │                               │
96
//   │           │                │           │                               │
97
//   │           │                            │           CLTV-Delayed        │
98
//   │           │                │           V            babyOutputs        │
99
//   │           │                        ┌──────┐                            │
100
//   │           │                │       │ CRIB │                            │
101
//   │           │                        └──────┘                            │
102
//   │           │                │           │                               │
103
//   │           │                            │                               │
104
//   │           │                │           |                               │
105
//   │           │                            V    Wait CLTV                  │
106
//   │           │                │          [ ]       +                      │
107
//   │           │                            |   Publish Txn                 │
108
//   │           │                │           │                               │
109
//   │           │                            │                               │
110
//   │           │                │           V ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┐        │
111
//   │           │                           ( )  waitForTimeoutConf          │
112
//   │           │                │           | └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘        │
113
//   │           │                            │                               │
114
//   │           │                │           │                               │
115
//   │           │                            │                               │
116
//   │           V                │           │                               │
117
//   │       ┌──────┐                         │                               │
118
//   │       │ PSCL │             └  ──  ──  ─┼  ──  ──  ──  ──  ──  ──  ──  ─┤
119
//   │       └──────┘                         │                               │
120
//   │           │                            │                               │
121
//   │           │                            │                               │
122
//   │           V ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┐     │            CSV-Delayed        │
123
//   │          ( )  waitForCommitConf        │             kidOutputs        │
124
//   │           | └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘     │                               │
125
//   │           │                            │                               │
126
//   │           │                            │                               │
127
//   │           │                            V                               │
128
//   │           │                        ┌──────┐                            │
129
//   │           └─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│ KNDR │                            │
130
//   │                                    └──────┘                            │
131
//   │                                        │                               │
132
//   │                                        │                               │
133
//   │                                        |                               │
134
//   │                                        V     Wait CSV                  │
135
//   │                                       [ ]       +                      │
136
//   │                                        |   Publish Txn                 │
137
//   │                                        │                               │
138
//   │                                        │                               │
139
//   │                                        V ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐         │
140
//   │                                       ( )  waitForSweepConf            │
141
//   │                                        | └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘         │
142
//   │                                        │                               │
143
//   │                                        │                               │
144
//   │                                        V                               │
145
//   │                                     ┌──────┐                           │
146
//   │                                     │ GRAD │                           │
147
//   │                                     └──────┘                           │
148
//   │                                        │                               │
149
//   │                                        │                               │
150
//   │                                        │                               │
151
//   └────────────────────────────────────────┼───────────────────────────────┘
152
//                                            │
153
//                                            │
154
//                                            │
155
//                                            │
156
//                                            V
157
//                                   ┌────────────────┐
158
//                                   │ Wallet Outputs │
159
//                                   └────────────────┘
160

161
var byteOrder = binary.BigEndian
162

163
const (
164
        // kgtnOutputConfTarget is the default confirmation target we'll use for
165
        // sweeps of CSV delayed outputs.
166
        kgtnOutputConfTarget = 6
167
)
168

169
var (
170
        // ErrContractNotFound is returned when the nursery is unable to
171
        // retrieve information about a queried contract.
172
        ErrContractNotFound = fmt.Errorf("unable to locate contract")
173
)
174

175
// NurseryConfig abstracts the required subsystems used by the utxo nursery. An
176
// instance of NurseryConfig is passed to newUtxoNursery during instantiation.
177
type NurseryConfig struct {
178
        // ChainIO is used by the utxo nursery to determine the current block
179
        // height, which drives the incubation of the nursery's outputs.
180
        ChainIO lnwallet.BlockChainIO
181

182
        // ConfDepth is the number of blocks the nursery store waits before
183
        // determining outputs in the chain as confirmed.
184
        ConfDepth uint32
185

186
        // FetchClosedChannels provides access to a user's channels, such that
187
        // they can be marked fully closed after incubation has concluded.
188
        FetchClosedChannels func(pendingOnly bool) (
189
                []*channeldb.ChannelCloseSummary, error)
190

191
        // FetchClosedChannel provides access to the close summary to extract a
192
        // height hint from.
193
        FetchClosedChannel func(chanID *wire.OutPoint) (
194
                *channeldb.ChannelCloseSummary, error)
195

196
        // Notifier provides the utxo nursery the ability to subscribe to
197
        // transaction confirmation events, which advance outputs through their
198
        // persistence state transitions.
199
        Notifier chainntnfs.ChainNotifier
200

201
        // PublishTransaction facilitates the process of broadcasting a signed
202
        // transaction to the appropriate network.
203
        PublishTransaction func(*wire.MsgTx, string) error
204

205
        // Store provides access to and modification of the persistent state
206
        // maintained about the utxo nursery's incubating outputs.
207
        Store NurseryStorer
208

209
        // Sweep sweeps an input back to the wallet.
210
        SweepInput func(input.Input, sweep.Params) (chan sweep.Result, error)
211

212
        // Budget is the configured budget for the nursery.
213
        Budget *BudgetConfig
214
}
215

216
// UtxoNursery is a system dedicated to incubating time-locked outputs created
217
// by the broadcast of a commitment transaction either by us, or the remote
218
// peer. The nursery accepts outputs and "incubates" them until they've reached
219
// maturity, then sweep the outputs into the source wallet. An output is
220
// considered mature after the relative time-lock within the pkScript has
221
// passed. As outputs reach their maturity age, they're swept in batches into
222
// the source wallet, returning the outputs so they can be used within future
223
// channels, or regular Bitcoin transactions.
224
type UtxoNursery struct {
225
        started uint32 // To be used atomically.
226
        stopped uint32 // To be used atomically.
227

228
        cfg *NurseryConfig
229

230
        mu         sync.Mutex
231
        bestHeight uint32
232

233
        quit chan struct{}
234
        wg   sync.WaitGroup
235
}
236

237
// NewUtxoNursery creates a new instance of the UtxoNursery from a
238
// ChainNotifier and LightningWallet instance.
239
func NewUtxoNursery(cfg *NurseryConfig) *UtxoNursery {
3✔
240
        return &UtxoNursery{
3✔
241
                cfg:  cfg,
3✔
242
                quit: make(chan struct{}),
3✔
243
        }
3✔
244
}
3✔
245

246
// Start launches all goroutines the UtxoNursery needs to properly carry out
247
// its duties.
248
func (u *UtxoNursery) Start() error {
3✔
249
        if !atomic.CompareAndSwapUint32(&u.started, 0, 1) {
3✔
250
                return nil
×
251
        }
×
252

253
        utxnLog.Info("UTXO nursery starting")
3✔
254

3✔
255
        // Retrieve the currently best known block. This is needed to have the
3✔
256
        // state machine catch up with the blocks we missed when we were down.
3✔
257
        bestHash, bestHeight, err := u.cfg.ChainIO.GetBestBlock()
3✔
258
        if err != nil {
3✔
259
                return err
×
260
        }
×
261

262
        // Set best known height to schedule late registrations properly.
263
        atomic.StoreUint32(&u.bestHeight, uint32(bestHeight))
3✔
264

3✔
265
        // 2. Flush all fully-graduated channels from the pipeline.
3✔
266

3✔
267
        // Load any pending close channels, which represents the super set of
3✔
268
        // all channels that may still be incubating.
3✔
269
        pendingCloseChans, err := u.cfg.FetchClosedChannels(true)
3✔
270
        if err != nil {
3✔
271
                return err
×
272
        }
×
273

274
        // Ensure that all mature channels have been marked as fully closed in
275
        // the channeldb.
276
        for _, pendingClose := range pendingCloseChans {
6✔
277
                err := u.closeAndRemoveIfMature(&pendingClose.ChanPoint)
3✔
278
                if err != nil {
3✔
279
                        return err
×
280
                }
×
281
        }
282

283
        // TODO(conner): check if any fully closed channels can be removed from
284
        // utxn.
285

286
        // 2. Restart spend ntfns for any preschool outputs, which are waiting
287
        // for the force closed commitment txn to confirm, or any second-layer
288
        // HTLC success transactions.
289
        //
290
        // NOTE: The next two steps *may* spawn go routines, thus from this
291
        // point forward, we must close the nursery's quit channel if we detect
292
        // any failures during startup to ensure they terminate.
293
        if err := u.reloadPreschool(); err != nil {
3✔
294
                close(u.quit)
×
295
                return err
×
296
        }
×
297

298
        // 3. Replay all crib and kindergarten outputs up to the current best
299
        // height.
300
        if err := u.reloadClasses(uint32(bestHeight)); err != nil {
3✔
301
                close(u.quit)
×
302
                return err
×
303
        }
×
304

305
        // Start watching for new blocks, as this will drive the nursery store's
306
        // state machine.
307
        newBlockChan, err := u.cfg.Notifier.RegisterBlockEpochNtfn(&chainntnfs.BlockEpoch{
3✔
308
                Height: bestHeight,
3✔
309
                Hash:   bestHash,
3✔
310
        })
3✔
311
        if err != nil {
3✔
312
                close(u.quit)
×
313
                return err
×
314
        }
×
315

316
        u.wg.Add(1)
3✔
317
        go u.incubator(newBlockChan)
3✔
318

3✔
319
        return nil
3✔
320
}
321

322
// Stop gracefully shuts down any lingering goroutines launched during normal
323
// operation of the UtxoNursery.
324
func (u *UtxoNursery) Stop() error {
3✔
325
        if !atomic.CompareAndSwapUint32(&u.stopped, 0, 1) {
3✔
326
                return nil
×
327
        }
×
328

329
        utxnLog.Infof("UTXO nursery shutting down...")
3✔
330
        defer utxnLog.Debug("UTXO nursery shutdown complete")
3✔
331

3✔
332
        close(u.quit)
3✔
333
        u.wg.Wait()
3✔
334

3✔
335
        return nil
3✔
336
}
337

338
// IncubateOutputs sends a request to the UtxoNursery to incubate a set of
339
// outputs from an existing commitment transaction. Outputs need to incubate if
340
// they're CLTV absolute time locked, or if they're CSV relative time locked.
341
// Once all outputs reach maturity, they'll be swept back into the wallet.
342
func (u *UtxoNursery) IncubateOutputs(chanPoint wire.OutPoint,
343
        outgoingHtlc fn.Option[lnwallet.OutgoingHtlcResolution],
344
        incomingHtlc fn.Option[lnwallet.IncomingHtlcResolution],
345
        broadcastHeight uint32, deadlineHeight fn.Option[int32]) error {
3✔
346

3✔
347
        // Add to wait group because nursery might shut down during execution of
3✔
348
        // this function. Otherwise it could happen that nursery thinks it is
3✔
349
        // shut down, but in this function new goroutines were started and stay
3✔
350
        // around.
3✔
351
        u.wg.Add(1)
3✔
352
        defer u.wg.Done()
3✔
353

3✔
354
        // Check quit channel for the case where the waitgroup wait was finished
3✔
355
        // right before this function's add call was made.
3✔
356
        select {
3✔
357
        case <-u.quit:
×
358
                return fmt.Errorf("nursery shutting down")
×
359
        default:
3✔
360
        }
361

362
        var (
3✔
363
                // Kid outputs can be swept after an initial confirmation
3✔
364
                // followed by a maturity period.Baby outputs are two stage and
3✔
365
                // will need to wait for an absolute time out to reach a
3✔
366
                // confirmation, then require a relative confirmation delay.
3✔
367
                kidOutputs  = make([]kidOutput, 0)
3✔
368
                babyOutputs = make([]babyOutput, 0)
3✔
369
        )
3✔
370

3✔
371
        // 1. Build all the spendable outputs that we will try to incubate.
3✔
372

3✔
373
        // TODO(roasbeef): query and see if we already have, if so don't add?
3✔
374

3✔
375
        // For each incoming HTLC, we'll register a kid output marked as a
3✔
376
        // second-layer HTLC output. We effectively skip the baby stage (as the
3✔
377
        // timelock is zero), and enter the kid stage.
3✔
378
        incomingHtlc.WhenSome(func(htlcRes lnwallet.IncomingHtlcResolution) {
3✔
379
                // Based on the input pk script of the sign descriptor, we can
×
380
                // determine if this is a taproot output or not. This'll
×
381
                // determine the witness type we try to set below.
×
382
                isTaproot := txscript.IsPayToTaproot(
×
383
                        htlcRes.SweepSignDesc.Output.PkScript,
×
384
                )
×
385

×
386
                var witType input.StandardWitnessType
×
387
                if isTaproot {
×
388
                        witType = input.TaprootHtlcAcceptedSuccessSecondLevel
×
389
                } else {
×
390
                        witType = input.HtlcAcceptedSuccessSecondLevel
×
391
                }
×
392

393
                htlcOutput := makeKidOutput(
×
394
                        &htlcRes.ClaimOutpoint, &chanPoint, htlcRes.CsvDelay,
×
395
                        witType, &htlcRes.SweepSignDesc, 0, deadlineHeight,
×
396
                )
×
397

×
398
                if htlcOutput.Amount() > 0 {
×
399
                        kidOutputs = append(kidOutputs, htlcOutput)
×
400
                }
×
401
        })
402

403
        // For each outgoing HTLC, we'll create a baby output. If this is our
404
        // commitment transaction, then we'll broadcast a second-layer
405
        // transaction to transition to a kid output. Otherwise, we'll directly
406
        // spend once the CLTV delay us up.
407
        outgoingHtlc.WhenSome(func(htlcRes lnwallet.OutgoingHtlcResolution) {
6✔
408
                // If this HTLC is on our commitment transaction, then it'll be
3✔
409
                // a baby output as we need to go to the second level to sweep
3✔
410
                // it.
3✔
411
                if htlcRes.SignedTimeoutTx != nil {
6✔
412
                        htlcOutput := makeBabyOutput(
3✔
413
                                &chanPoint, &htlcRes, deadlineHeight,
3✔
414
                        )
3✔
415

3✔
416
                        if htlcOutput.Amount() > 0 {
6✔
417
                                babyOutputs = append(babyOutputs, htlcOutput)
3✔
418
                        }
3✔
419

420
                        return
3✔
421
                }
422

423
                // Based on the input pk script of the sign descriptor, we can
424
                // determine if this is a taproot output or not. This'll
425
                // determine the witness type we try to set below.
UNCOV
426
                isTaproot := txscript.IsPayToTaproot(
×
UNCOV
427
                        htlcRes.SweepSignDesc.Output.PkScript,
×
UNCOV
428
                )
×
UNCOV
429

×
UNCOV
430
                var witType input.StandardWitnessType
×
UNCOV
431
                if isTaproot {
×
432
                        witType = input.TaprootHtlcOfferedRemoteTimeout
×
UNCOV
433
                } else {
×
UNCOV
434
                        witType = input.HtlcOfferedRemoteTimeout
×
UNCOV
435
                }
×
436

437
                // Otherwise, this is actually a kid output as we can sweep it
438
                // once the commitment transaction confirms, and the absolute
439
                // CLTV lock has expired. We set the CSV delay what the
440
                // resolution encodes, since the sequence number must be set
441
                // accordingly.
UNCOV
442
                htlcOutput := makeKidOutput(
×
UNCOV
443
                        &htlcRes.ClaimOutpoint, &chanPoint, htlcRes.CsvDelay,
×
UNCOV
444
                        witType, &htlcRes.SweepSignDesc, htlcRes.Expiry,
×
UNCOV
445
                        deadlineHeight,
×
UNCOV
446
                )
×
UNCOV
447
                kidOutputs = append(kidOutputs, htlcOutput)
×
448
        })
449

450
        // TODO(roasbeef): if want to handle outgoing on remote commit
451
        //  * need ability to cancel in the case that we learn of pre-image or
452
        //    remote party pulls
453

454
        numHtlcs := len(babyOutputs) + len(kidOutputs)
3✔
455
        utxnLog.Infof("Incubating Channel(%s) num-htlcs=%d",
3✔
456
                chanPoint, numHtlcs)
3✔
457

3✔
458
        u.mu.Lock()
3✔
459
        defer u.mu.Unlock()
3✔
460

3✔
461
        // 2. Persist the outputs we intended to sweep in the nursery store
3✔
462
        if err := u.cfg.Store.Incubate(kidOutputs, babyOutputs); err != nil {
3✔
463
                utxnLog.Errorf("unable to begin incubation of Channel(%s): %v",
×
464
                        chanPoint, err)
×
465
                return err
×
466
        }
×
467

468
        // As an intermediate step, we'll now check to see if any of the baby
469
        // outputs has actually _already_ expired. This may be the case if
470
        // blocks were mined while we processed this message.
471
        _, bestHeight, err := u.cfg.ChainIO.GetBestBlock()
3✔
472
        if err != nil {
3✔
473
                return err
×
474
        }
×
475

476
        // We'll examine all the baby outputs just inserted into the database,
477
        // if the output has already expired, then we'll *immediately* sweep
478
        // it. This may happen if the caller raced a block to call this method.
479
        for i, babyOutput := range babyOutputs {
6✔
480
                if uint32(bestHeight) >= babyOutput.expiry {
6✔
481
                        err = u.sweepCribOutput(
3✔
482
                                babyOutput.expiry, &babyOutputs[i],
3✔
483
                        )
3✔
484
                        if err != nil {
3✔
UNCOV
485
                                return err
×
UNCOV
486
                        }
×
487
                }
488
        }
489

490
        // 3. If we are incubating any preschool outputs, register for a
491
        // confirmation notification that will transition it to the
492
        // kindergarten bucket.
493
        if len(kidOutputs) != 0 {
3✔
UNCOV
494
                for i := range kidOutputs {
×
UNCOV
495
                        err := u.registerPreschoolConf(
×
UNCOV
496
                                &kidOutputs[i], broadcastHeight,
×
UNCOV
497
                        )
×
UNCOV
498
                        if err != nil {
×
499
                                return err
×
500
                        }
×
501
                }
502
        }
503

504
        return nil
3✔
505
}
506

507
// NurseryReport attempts to return a nursery report stored for the target
508
// outpoint. A nursery report details the maturity/sweeping progress for a
509
// contract that was previously force closed. If a report entry for the target
510
// chanPoint is unable to be constructed, then an error will be returned.
511
func (u *UtxoNursery) NurseryReport(
512
        chanPoint *wire.OutPoint) (*ContractMaturityReport, error) {
3✔
513

3✔
514
        u.mu.Lock()
3✔
515
        defer u.mu.Unlock()
3✔
516

3✔
517
        utxnLog.Debugf("NurseryReport: building nursery report for channel %v",
3✔
518
                chanPoint)
3✔
519

3✔
520
        var report *ContractMaturityReport
3✔
521

3✔
522
        if err := u.cfg.Store.ForChanOutputs(chanPoint, func(k, v []byte) error {
3✔
UNCOV
523
                switch {
×
UNCOV
524
                case bytes.HasPrefix(k, cribPrefix):
×
UNCOV
525
                        // Cribs outputs are the only kind currently stored as
×
UNCOV
526
                        // baby outputs.
×
UNCOV
527
                        var baby babyOutput
×
UNCOV
528
                        err := baby.Decode(bytes.NewReader(v))
×
UNCOV
529
                        if err != nil {
×
530
                                return err
×
531
                        }
×
532

533
                        // Each crib output represents a stage one htlc, and
534
                        // will contribute towards the limbo balance.
UNCOV
535
                        report.AddLimboStage1TimeoutHtlc(&baby)
×
536

537
                case bytes.HasPrefix(k, psclPrefix),
538
                        bytes.HasPrefix(k, kndrPrefix),
UNCOV
539
                        bytes.HasPrefix(k, gradPrefix):
×
UNCOV
540

×
UNCOV
541
                        // All others states can be deserialized as kid outputs.
×
UNCOV
542
                        var kid kidOutput
×
UNCOV
543
                        err := kid.Decode(bytes.NewReader(v))
×
UNCOV
544
                        if err != nil {
×
545
                                return err
×
546
                        }
×
547

548
                        // Now, use the state prefixes to determine how the
549
                        // this output should be represented in the nursery
550
                        // report.  An output's funds are always in limbo until
551
                        // reaching the graduate state.
UNCOV
552
                        switch {
×
UNCOV
553
                        case bytes.HasPrefix(k, psclPrefix):
×
UNCOV
554
                                // Preschool outputs are awaiting the
×
UNCOV
555
                                // confirmation of the commitment transaction.
×
UNCOV
556
                                switch kid.WitnessType() {
×
557

558
                                //nolint:ll
559
                                case input.TaprootHtlcAcceptedSuccessSecondLevel:
×
560
                                        fallthrough
×
561
                                case input.HtlcAcceptedSuccessSecondLevel:
×
562
                                        // An HTLC output on our commitment
×
563
                                        // transaction where the second-layer
×
564
                                        // transaction hasn't
×
565
                                        // yet confirmed.
×
566
                                        report.AddLimboStage1SuccessHtlc(&kid)
×
567

568
                                case input.HtlcOfferedRemoteTimeout,
UNCOV
569
                                        input.TaprootHtlcOfferedRemoteTimeout:
×
UNCOV
570
                                        // This is an HTLC output on the
×
UNCOV
571
                                        // commitment transaction of the remote
×
UNCOV
572
                                        // party. We are waiting for the CLTV
×
UNCOV
573
                                        // timelock expire.
×
UNCOV
574
                                        report.AddLimboDirectHtlc(&kid)
×
575
                                }
576

UNCOV
577
                        case bytes.HasPrefix(k, kndrPrefix):
×
UNCOV
578
                                // Kindergarten outputs may originate from
×
UNCOV
579
                                // either the commitment transaction or an htlc.
×
UNCOV
580
                                // We can distinguish them via their witness
×
UNCOV
581
                                // types.
×
UNCOV
582
                                switch kid.WitnessType() {
×
583

584
                                case input.HtlcOfferedRemoteTimeout,
UNCOV
585
                                        input.TaprootHtlcOfferedRemoteTimeout:
×
UNCOV
586
                                        // This is an HTLC output on the
×
UNCOV
587
                                        // commitment transaction of the remote
×
UNCOV
588
                                        // party. The CLTV timelock has
×
UNCOV
589
                                        // expired, and we only need to sweep
×
UNCOV
590
                                        // it.
×
UNCOV
591
                                        report.AddLimboDirectHtlc(&kid)
×
592

593
                                //nolint:ll
594
                                case input.TaprootHtlcAcceptedSuccessSecondLevel:
×
595
                                        fallthrough
×
596
                                case input.TaprootHtlcOfferedTimeoutSecondLevel:
×
597
                                        fallthrough
×
598
                                case input.HtlcAcceptedSuccessSecondLevel:
×
599
                                        fallthrough
×
UNCOV
600
                                case input.HtlcOfferedTimeoutSecondLevel:
×
UNCOV
601
                                        // The htlc timeout or success
×
UNCOV
602
                                        // transaction has confirmed, and the
×
UNCOV
603
                                        // CSV delay has begun ticking.
×
UNCOV
604
                                        report.AddLimboStage2Htlc(&kid)
×
605
                                }
606

607
                        case bytes.HasPrefix(k, gradPrefix):
×
608
                                // Graduate outputs are those whose funds have
×
609
                                // been swept back into the wallet. Each output
×
610
                                // will contribute towards the recovered
×
611
                                // balance.
×
612
                                switch kid.WitnessType() {
×
613

614
                                //nolint:ll
615
                                case input.TaprootHtlcAcceptedSuccessSecondLevel:
×
616
                                        fallthrough
×
617
                                case input.TaprootHtlcOfferedTimeoutSecondLevel:
×
618
                                        fallthrough
×
619
                                case input.HtlcAcceptedSuccessSecondLevel:
×
620
                                        fallthrough
×
621
                                case input.HtlcOfferedTimeoutSecondLevel:
×
622
                                        fallthrough
×
623
                                case input.TaprootHtlcOfferedRemoteTimeout:
×
624
                                        fallthrough
×
625
                                case input.HtlcOfferedRemoteTimeout:
×
626
                                        // This htlc output successfully
×
627
                                        // resides in a p2wkh output belonging
×
628
                                        // to the user.
×
629
                                        report.AddRecoveredHtlc(&kid)
×
630
                                }
631
                        }
632

633
                default:
×
634
                }
635

UNCOV
636
                return nil
×
637
        }, func() {
3✔
638
                report = &ContractMaturityReport{}
3✔
639
        }); err != nil {
6✔
640
                return nil, err
3✔
641
        }
3✔
642

UNCOV
643
        return report, nil
×
644
}
645

646
// reloadPreschool re-initializes the chain notifier with all of the outputs
647
// that had been saved to the "preschool" database bucket prior to shutdown.
648
func (u *UtxoNursery) reloadPreschool() error {
3✔
649
        psclOutputs, err := u.cfg.Store.FetchPreschools()
3✔
650
        if err != nil {
3✔
651
                return err
×
652
        }
×
653

654
        // For each of the preschool outputs stored in the nursery store, load
655
        // its close summary from disk so that we can get an accurate height
656
        // hint from which to start our range for spend notifications.
657
        for i := range psclOutputs {
3✔
UNCOV
658
                kid := &psclOutputs[i]
×
UNCOV
659
                chanPoint := kid.OriginChanPoint()
×
UNCOV
660

×
UNCOV
661
                // Load the close summary for this output's channel point.
×
UNCOV
662
                closeSummary, err := u.cfg.FetchClosedChannel(chanPoint)
×
UNCOV
663
                if err == channeldb.ErrClosedChannelNotFound {
×
664
                        // This should never happen since the close summary
×
665
                        // should only be removed after the channel has been
×
666
                        // swept completely.
×
667
                        utxnLog.Warnf("Close summary not found for "+
×
668
                                "chan_point=%v, can't determine height hint"+
×
669
                                "to sweep commit txn", chanPoint)
×
670
                        continue
×
671

UNCOV
672
                } else if err != nil {
×
673
                        return err
×
674
                }
×
675

676
                // Use the close height from the channel summary as our height
677
                // hint to drive our spend notifications, with our confirmation
678
                // depth as a buffer for reorgs.
UNCOV
679
                heightHint := closeSummary.CloseHeight - u.cfg.ConfDepth
×
UNCOV
680
                err = u.registerPreschoolConf(kid, heightHint)
×
UNCOV
681
                if err != nil {
×
682
                        return err
×
683
                }
×
684
        }
685

686
        return nil
3✔
687
}
688

689
// reloadClasses reinitializes any height-dependent state transitions for which
690
// the utxonursery has not received confirmation, and replays the graduation of
691
// all kindergarten and crib outputs for all heights up to the current block.
692
// This allows the nursery to reinitialize all state to continue sweeping
693
// outputs, even in the event that we missed blocks while offline. reloadClasses
694
// is called during the startup of the UTXO Nursery.
695
func (u *UtxoNursery) reloadClasses(bestHeight uint32) error {
3✔
696
        // Loading all active heights up to and including the current block.
3✔
697
        activeHeights, err := u.cfg.Store.HeightsBelowOrEqual(
3✔
698
                uint32(bestHeight))
3✔
699
        if err != nil {
3✔
700
                return err
×
701
        }
×
702

703
        // Return early if nothing to sweep.
704
        if len(activeHeights) == 0 {
6✔
705
                return nil
3✔
706
        }
3✔
707

UNCOV
708
        utxnLog.Infof("(Re)-sweeping %d heights below height=%d",
×
UNCOV
709
                len(activeHeights), bestHeight)
×
UNCOV
710

×
UNCOV
711
        // Attempt to re-register notifications for any outputs still at these
×
UNCOV
712
        // heights.
×
UNCOV
713
        for _, classHeight := range activeHeights {
×
UNCOV
714
                utxnLog.Debugf("Attempting to sweep outputs at height=%v",
×
UNCOV
715
                        classHeight)
×
UNCOV
716

×
UNCOV
717
                if err = u.graduateClass(classHeight); err != nil {
×
718
                        utxnLog.Errorf("Failed to sweep outputs at "+
×
719
                                "height=%v: %v", classHeight, err)
×
720
                        return err
×
721
                }
×
722
        }
723

UNCOV
724
        utxnLog.Infof("UTXO Nursery is now fully synced")
×
UNCOV
725

×
UNCOV
726
        return nil
×
727
}
728

729
// incubator is tasked with driving all state transitions that are dependent on
730
// the current height of the blockchain. As new blocks arrive, the incubator
731
// will attempt spend outputs at the latest height. The asynchronous
732
// confirmation of these spends will either 1) move a crib output into the
733
// kindergarten bucket or 2) move a kindergarten output into the graduated
734
// bucket.
735
func (u *UtxoNursery) incubator(newBlockChan *chainntnfs.BlockEpochEvent) {
3✔
736
        defer u.wg.Done()
3✔
737
        defer newBlockChan.Cancel()
3✔
738

3✔
739
        for {
6✔
740
                select {
3✔
741
                case epoch, ok := <-newBlockChan.Epochs:
3✔
742
                        // If the epoch channel has been closed, then the
3✔
743
                        // ChainNotifier is exiting which means the daemon is
3✔
744
                        // as well. Therefore, we exit early also in order to
3✔
745
                        // ensure the daemon shuts down gracefully, yet
3✔
746
                        // swiftly.
3✔
747
                        if !ok {
3✔
748
                                return
×
749
                        }
×
750

751
                        // TODO(roasbeef): if the BlockChainIO is rescanning
752
                        // will give stale data
753

754
                        // A new block has just been connected to the main
755
                        // chain, which means we might be able to graduate crib
756
                        // or kindergarten outputs at this height. This involves
757
                        // broadcasting any presigned htlc timeout txns, as well
758
                        // as signing and broadcasting a sweep txn that spends
759
                        // from all kindergarten outputs at this height.
760
                        height := uint32(epoch.Height)
3✔
761

3✔
762
                        // Update best known block height for late registrations
3✔
763
                        // to be scheduled properly.
3✔
764
                        atomic.StoreUint32(&u.bestHeight, height)
3✔
765

3✔
766
                        if err := u.graduateClass(height); err != nil {
3✔
UNCOV
767
                                utxnLog.Errorf("error while graduating "+
×
UNCOV
768
                                        "class at height=%d: %v", height, err)
×
UNCOV
769

×
UNCOV
770
                                // TODO(conner): signal fatal error to daemon
×
UNCOV
771
                        }
×
772

773
                case <-u.quit:
3✔
774
                        return
3✔
775
                }
776
        }
777
}
778

779
// graduateClass handles the steps involved in spending outputs whose CSV or
780
// CLTV delay expires at the nursery's current height. This method is called
781
// each time a new block arrives, or during startup to catch up on heights we
782
// may have missed while the nursery was offline.
783
func (u *UtxoNursery) graduateClass(classHeight uint32) error {
3✔
784
        // Record this height as the nursery's current best height.
3✔
785
        u.mu.Lock()
3✔
786
        defer u.mu.Unlock()
3✔
787

3✔
788
        // Fetch all information about the crib and kindergarten outputs at
3✔
789
        // this height.
3✔
790
        kgtnOutputs, cribOutputs, err := u.cfg.Store.FetchClass(
3✔
791
                classHeight,
3✔
792
        )
3✔
793
        if err != nil {
3✔
UNCOV
794
                return err
×
UNCOV
795
        }
×
796

797
        utxnLog.Debugf("Attempting to graduate height=%v: num_kids=%v, "+
3✔
798
                "num_babies=%v", classHeight, len(kgtnOutputs), len(cribOutputs))
3✔
799

3✔
800
        // Offer the outputs to the sweeper and set up notifications that will
3✔
801
        // transition the swept kindergarten outputs and cltvCrib into graduated
3✔
802
        // outputs.
3✔
803
        if len(kgtnOutputs) > 0 {
3✔
UNCOV
804
                if err := u.sweepMatureOutputs(classHeight, kgtnOutputs); err != nil {
×
805
                        utxnLog.Errorf("Failed to sweep %d kindergarten "+
×
806
                                "outputs at height=%d: %v",
×
807
                                len(kgtnOutputs), classHeight, err)
×
808
                        return err
×
809
                }
×
810
        }
811

812
        // Now, we broadcast all pre-signed htlc txns from the csv crib outputs
813
        // at this height.
814
        for i := range cribOutputs {
6✔
815
                err := u.sweepCribOutput(classHeight, &cribOutputs[i])
3✔
816
                if err != nil {
3✔
817
                        utxnLog.Errorf("Failed to sweep first-stage HTLC "+
×
818
                                "(CLTV-delayed) output %v",
×
819
                                cribOutputs[i].OutPoint())
×
820
                        return err
×
821
                }
×
822
        }
823

824
        return nil
3✔
825
}
826

827
// decideDeadlineAndBudget returns the deadline and budget for a given output.
828
func (u *UtxoNursery) decideDeadlineAndBudget(k kidOutput) (fn.Option[int32],
UNCOV
829
        btcutil.Amount) {
×
UNCOV
830

×
UNCOV
831
        // Assume this is a to_local output and use a None deadline.
×
UNCOV
832
        deadline := fn.None[int32]()
×
UNCOV
833

×
UNCOV
834
        // Exit early if this is not HTLC.
×
UNCOV
835
        if !k.isHtlc {
×
UNCOV
836
                budget := calculateBudget(
×
UNCOV
837
                        k.amt, u.cfg.Budget.ToLocalRatio, u.cfg.Budget.ToLocal,
×
UNCOV
838
                )
×
UNCOV
839

×
UNCOV
840
                return deadline, budget
×
UNCOV
841
        }
×
842

843
        // Otherwise it's the first-level HTLC output, we'll use the
844
        // time-sensitive settings for it.
UNCOV
845
        budget := calculateBudget(
×
UNCOV
846
                k.amt, u.cfg.Budget.DeadlineHTLCRatio,
×
UNCOV
847
                u.cfg.Budget.DeadlineHTLC,
×
UNCOV
848
        )
×
UNCOV
849

×
UNCOV
850
        return k.deadlineHeight, budget
×
851
}
852

853
// sweepMatureOutputs generates and broadcasts the transaction that transfers
854
// control of funds from a prior channel commitment transaction to the user's
855
// wallet. The outputs swept were previously time locked (either absolute or
856
// relative), but are not mature enough to sweep into the wallet.
857
func (u *UtxoNursery) sweepMatureOutputs(classHeight uint32,
UNCOV
858
        kgtnOutputs []kidOutput) error {
×
UNCOV
859

×
UNCOV
860
        utxnLog.Infof("Sweeping %v CSV-delayed outputs with sweep tx for "+
×
UNCOV
861
                "height %v", len(kgtnOutputs), classHeight)
×
UNCOV
862

×
UNCOV
863
        for _, output := range kgtnOutputs {
×
UNCOV
864
                // Create local copy to prevent pointer to loop variable to be
×
UNCOV
865
                // passed in with disastrous consequences.
×
UNCOV
866
                local := output
×
UNCOV
867

×
UNCOV
868
                // Calculate the deadline height and budget for this output.
×
UNCOV
869
                deadline, budget := u.decideDeadlineAndBudget(local)
×
UNCOV
870

×
UNCOV
871
                resultChan, err := u.cfg.SweepInput(&local, sweep.Params{
×
UNCOV
872
                        DeadlineHeight: deadline,
×
UNCOV
873
                        Budget:         budget,
×
UNCOV
874
                })
×
UNCOV
875
                if err != nil {
×
876
                        return err
×
877
                }
×
UNCOV
878
                u.wg.Add(1)
×
UNCOV
879
                go u.waitForSweepConf(classHeight, &local, resultChan)
×
880
        }
881

UNCOV
882
        return nil
×
883
}
884

885
// waitForSweepConf watches for the confirmation of a sweep transaction
886
// containing a batch of kindergarten outputs. Once confirmation has been
887
// received, the nursery will mark those outputs as fully graduated, and proceed
888
// to mark any mature channels as fully closed in channeldb.
889
// NOTE(conner): this method MUST be called as a go routine.
890
func (u *UtxoNursery) waitForSweepConf(classHeight uint32,
UNCOV
891
        output *kidOutput, resultChan chan sweep.Result) {
×
UNCOV
892

×
UNCOV
893
        defer u.wg.Done()
×
UNCOV
894

×
UNCOV
895
        select {
×
UNCOV
896
        case result, ok := <-resultChan:
×
UNCOV
897
                if !ok {
×
898
                        utxnLog.Errorf("Notification chan closed, can't" +
×
899
                                " advance graduating output")
×
900
                        return
×
901
                }
×
902

903
                // In case of a remote spend, still graduate the output. There
904
                // is no way to sweep it anymore.
UNCOV
905
                if result.Err == sweep.ErrRemoteSpend {
×
906
                        utxnLog.Infof("Output %v was spend by remote party",
×
907
                                output.OutPoint())
×
908
                        break
×
909
                }
910

UNCOV
911
                if result.Err != nil {
×
912
                        utxnLog.Errorf("Failed to sweep %v at "+
×
913
                                "height=%d", output.OutPoint(),
×
914
                                classHeight)
×
915
                        return
×
916
                }
×
917

UNCOV
918
        case <-u.quit:
×
UNCOV
919
                return
×
920
        }
921

UNCOV
922
        u.mu.Lock()
×
UNCOV
923
        defer u.mu.Unlock()
×
UNCOV
924

×
UNCOV
925
        // TODO(conner): add retry utxnLogic?
×
UNCOV
926

×
UNCOV
927
        // Mark the confirmed kindergarten output as graduated.
×
UNCOV
928
        if err := u.cfg.Store.GraduateKinder(classHeight, output); err != nil {
×
929
                utxnLog.Errorf("Unable to graduate kindergarten output %v: %v",
×
930
                        output.OutPoint(), err)
×
931
                return
×
932
        }
×
933

UNCOV
934
        utxnLog.Infof("Graduated kindergarten output from height=%d",
×
UNCOV
935
                classHeight)
×
UNCOV
936

×
UNCOV
937
        // Attempt to close the channel, only doing so if all of the channel's
×
UNCOV
938
        // outputs have been graduated.
×
UNCOV
939
        chanPoint := output.OriginChanPoint()
×
UNCOV
940
        if err := u.closeAndRemoveIfMature(chanPoint); err != nil {
×
941
                utxnLog.Errorf("Failed to close and remove channel %v",
×
942
                        *chanPoint)
×
943
                return
×
944
        }
×
945
}
946

947
// sweepCribOutput broadcasts the crib output's htlc timeout txn, and sets up a
948
// notification that will advance it to the kindergarten bucket upon
949
// confirmation.
950
func (u *UtxoNursery) sweepCribOutput(classHeight uint32, baby *babyOutput) error {
3✔
951
        utxnLog.Infof("Publishing CLTV-delayed HTLC output using timeout tx "+
3✔
952
                "(txid=%v): %v", baby.timeoutTx.TxHash(),
3✔
953
                lnutils.SpewLogClosure(baby.timeoutTx))
3✔
954

3✔
955
        // We'll now broadcast the HTLC transaction, then wait for it to be
3✔
956
        // confirmed before transitioning it to kindergarten.
3✔
957
        label := labels.MakeLabel(labels.LabelTypeSweepTransaction, nil)
3✔
958
        err := u.cfg.PublishTransaction(baby.timeoutTx, label)
3✔
959

3✔
960
        // In case the tx does not meet mempool fee requirements we continue
3✔
961
        // because the tx is rebroadcasted in the background and there is
3✔
962
        // nothing we can do to bump this transaction anyways.
3✔
963
        if err != nil && !errors.Is(err, lnwallet.ErrDoubleSpend) &&
3✔
964
                !errors.Is(err, lnwallet.ErrMempoolFee) {
3✔
UNCOV
965

×
UNCOV
966
                utxnLog.Errorf("Unable to broadcast baby tx: "+
×
UNCOV
967
                        "%v, %v", err, spew.Sdump(baby.timeoutTx))
×
UNCOV
968
                return err
×
UNCOV
969
        }
×
970

971
        return u.registerTimeoutConf(baby, classHeight)
3✔
972
}
973

974
// registerTimeoutConf is responsible for subscribing to confirmation
975
// notification for an htlc timeout transaction. If successful, a goroutine
976
// will be spawned that will transition the provided baby output into the
977
// kindergarten state within the nursery store.
978
func (u *UtxoNursery) registerTimeoutConf(baby *babyOutput,
979
        heightHint uint32) error {
3✔
980

3✔
981
        birthTxID := baby.timeoutTx.TxHash()
3✔
982

3✔
983
        // Register for the confirmation of presigned htlc txn.
3✔
984
        confChan, err := u.cfg.Notifier.RegisterConfirmationsNtfn(
3✔
985
                &birthTxID, baby.timeoutTx.TxOut[0].PkScript, u.cfg.ConfDepth,
3✔
986
                heightHint,
3✔
987
        )
3✔
988
        if err != nil {
3✔
989
                return err
×
990
        }
×
991

992
        utxnLog.Infof("Htlc output %v registered for promotion "+
3✔
993
                "notification.", baby.OutPoint())
3✔
994

3✔
995
        u.wg.Add(1)
3✔
996
        go u.waitForTimeoutConf(baby, confChan)
3✔
997

3✔
998
        return nil
3✔
999
}
1000

1001
// waitForTimeoutConf watches for the confirmation of an htlc timeout
1002
// transaction, and attempts to move the htlc output from the crib bucket to the
1003
// kindergarten bucket upon success.
1004
func (u *UtxoNursery) waitForTimeoutConf(baby *babyOutput,
1005
        confChan *chainntnfs.ConfirmationEvent) {
3✔
1006

3✔
1007
        defer u.wg.Done()
3✔
1008

3✔
1009
        select {
3✔
1010
        case txConfirmation, ok := <-confChan.Confirmed:
3✔
1011
                if !ok {
3✔
1012
                        utxnLog.Debugf("Notification chan "+
×
1013
                                "closed, can't advance baby output %v",
×
1014
                                baby.OutPoint())
×
1015
                        return
×
1016
                }
×
1017

1018
                baby.SetConfHeight(txConfirmation.BlockHeight)
3✔
1019

UNCOV
1020
        case <-u.quit:
×
UNCOV
1021
                return
×
1022
        }
1023

1024
        u.mu.Lock()
3✔
1025
        defer u.mu.Unlock()
3✔
1026

3✔
1027
        // TODO(conner): add retry utxnLogic?
3✔
1028

3✔
1029
        err := u.cfg.Store.CribToKinder(baby)
3✔
1030
        if err != nil {
3✔
1031
                utxnLog.Errorf("Unable to move htlc output from "+
×
1032
                        "crib to kindergarten bucket: %v", err)
×
1033
                return
×
1034
        }
×
1035

1036
        utxnLog.Infof("Htlc output %v promoted to "+
3✔
1037
                "kindergarten", baby.OutPoint())
3✔
1038
}
1039

1040
// registerPreschoolConf is responsible for subscribing to the confirmation of
1041
// a commitment transaction, or an htlc success transaction for an incoming
1042
// HTLC on our commitment transaction.. If successful, the provided preschool
1043
// output will be moved persistently into the kindergarten state within the
1044
// nursery store.
UNCOV
1045
func (u *UtxoNursery) registerPreschoolConf(kid *kidOutput, heightHint uint32) error {
×
UNCOV
1046
        txID := kid.OutPoint().Hash
×
UNCOV
1047

×
UNCOV
1048
        // TODO(roasbeef): ensure we don't already have one waiting, need to
×
UNCOV
1049
        // de-duplicate
×
UNCOV
1050
        //  * need to do above?
×
UNCOV
1051

×
UNCOV
1052
        pkScript := kid.signDesc.Output.PkScript
×
UNCOV
1053
        confChan, err := u.cfg.Notifier.RegisterConfirmationsNtfn(
×
UNCOV
1054
                &txID, pkScript, u.cfg.ConfDepth, heightHint,
×
UNCOV
1055
        )
×
UNCOV
1056
        if err != nil {
×
1057
                return err
×
1058
        }
×
1059

UNCOV
1060
        var outputType string
×
UNCOV
1061
        if kid.isHtlc {
×
UNCOV
1062
                outputType = "HTLC"
×
UNCOV
1063
        } else {
×
1064
                outputType = "Commitment"
×
1065
        }
×
1066

UNCOV
1067
        utxnLog.Infof("%v outpoint %v registered for "+
×
UNCOV
1068
                "confirmation notification.", outputType, kid.OutPoint())
×
UNCOV
1069

×
UNCOV
1070
        u.wg.Add(1)
×
UNCOV
1071
        go u.waitForPreschoolConf(kid, confChan)
×
UNCOV
1072

×
UNCOV
1073
        return nil
×
1074
}
1075

1076
// waitForPreschoolConf is intended to be run as a goroutine that will wait until
1077
// a channel force close commitment transaction, or a second layer HTLC success
1078
// transaction has been included in a confirmed block. Once the transaction has
1079
// been confirmed (as reported by the Chain Notifier), waitForPreschoolConf
1080
// will delete the output from the "preschool" database bucket and atomically
1081
// add it to the "kindergarten" database bucket.  This is the second step in
1082
// the output incubation process.
1083
func (u *UtxoNursery) waitForPreschoolConf(kid *kidOutput,
UNCOV
1084
        confChan *chainntnfs.ConfirmationEvent) {
×
UNCOV
1085

×
UNCOV
1086
        defer u.wg.Done()
×
UNCOV
1087

×
UNCOV
1088
        select {
×
UNCOV
1089
        case txConfirmation, ok := <-confChan.Confirmed:
×
UNCOV
1090
                if !ok {
×
1091
                        utxnLog.Errorf("Notification chan "+
×
1092
                                "closed, can't advance output %v",
×
1093
                                kid.OutPoint())
×
1094
                        return
×
1095
                }
×
1096

UNCOV
1097
                kid.SetConfHeight(txConfirmation.BlockHeight)
×
1098

UNCOV
1099
        case <-u.quit:
×
UNCOV
1100
                return
×
1101
        }
1102

UNCOV
1103
        u.mu.Lock()
×
UNCOV
1104
        defer u.mu.Unlock()
×
UNCOV
1105

×
UNCOV
1106
        // TODO(conner): add retry utxnLogic?
×
UNCOV
1107

×
UNCOV
1108
        var outputType string
×
UNCOV
1109
        if kid.isHtlc {
×
UNCOV
1110
                outputType = "HTLC"
×
UNCOV
1111
        } else {
×
1112
                outputType = "Commitment"
×
1113
        }
×
1114

UNCOV
1115
        bestHeight := atomic.LoadUint32(&u.bestHeight)
×
UNCOV
1116
        err := u.cfg.Store.PreschoolToKinder(kid, bestHeight)
×
UNCOV
1117
        if err != nil {
×
1118
                utxnLog.Errorf("Unable to move %v output "+
×
1119
                        "from preschool to kindergarten bucket: %v",
×
1120
                        outputType, err)
×
1121
                return
×
1122
        }
×
1123
}
1124

1125
// RemoveChannel channel erases all entries from the channel bucket for the
1126
// provided channel point.
1127
func (u *UtxoNursery) RemoveChannel(op *wire.OutPoint) error {
3✔
1128
        return u.cfg.Store.RemoveChannel(op)
3✔
1129
}
3✔
1130

1131
// ContractMaturityReport is a report that details the maturity progress of a
1132
// particular force closed contract.
1133
type ContractMaturityReport struct {
1134
        // limboBalance is the total number of frozen coins within this
1135
        // contract.
1136
        LimboBalance btcutil.Amount
1137

1138
        // recoveredBalance is the total value that has been successfully swept
1139
        // back to the user's wallet.
1140
        RecoveredBalance btcutil.Amount
1141

1142
        // htlcs records a maturity report for each htlc output in this channel.
1143
        Htlcs []HtlcMaturityReport
1144
}
1145

1146
// HtlcMaturityReport provides a summary of a single htlc output, and is
1147
// embedded as party of the overarching ContractMaturityReport.
1148
type HtlcMaturityReport struct {
1149
        // Outpoint is the final output that will be swept back to the wallet.
1150
        Outpoint wire.OutPoint
1151

1152
        // Amount is the final value that will be swept in back to the wallet.
1153
        Amount btcutil.Amount
1154

1155
        // MaturityHeight is the absolute block height that this output will
1156
        // mature at.
1157
        MaturityHeight uint32
1158

1159
        // Stage indicates whether the htlc is in the CLTV-timeout stage (1) or
1160
        // the CSV-delay stage (2). A stage 1 htlc's maturity height will be set
1161
        // to its expiry height, while a stage 2 htlc's maturity height will be
1162
        // set to its confirmation height plus the maturity requirement.
1163
        Stage uint32
1164
}
1165

1166
// AddLimboStage1TimeoutHtlc adds an htlc crib output to the maturity report's
1167
// htlcs, and contributes its amount to the limbo balance.
UNCOV
1168
func (c *ContractMaturityReport) AddLimboStage1TimeoutHtlc(baby *babyOutput) {
×
UNCOV
1169
        c.LimboBalance += baby.Amount()
×
UNCOV
1170

×
UNCOV
1171
        // TODO(roasbeef): bool to indicate stage 1 vs stage 2?
×
UNCOV
1172
        c.Htlcs = append(c.Htlcs, HtlcMaturityReport{
×
UNCOV
1173
                Outpoint:       baby.OutPoint(),
×
UNCOV
1174
                Amount:         baby.Amount(),
×
UNCOV
1175
                MaturityHeight: baby.expiry,
×
UNCOV
1176
                Stage:          1,
×
UNCOV
1177
        })
×
UNCOV
1178
}
×
1179

1180
// AddLimboDirectHtlc adds a direct HTLC on the commitment transaction of the
1181
// remote party to the maturity report. This a CLTV time-locked output that
1182
// has or hasn't expired yet.
UNCOV
1183
func (c *ContractMaturityReport) AddLimboDirectHtlc(kid *kidOutput) {
×
UNCOV
1184
        c.LimboBalance += kid.Amount()
×
UNCOV
1185

×
UNCOV
1186
        htlcReport := HtlcMaturityReport{
×
UNCOV
1187
                Outpoint:       kid.OutPoint(),
×
UNCOV
1188
                Amount:         kid.Amount(),
×
UNCOV
1189
                MaturityHeight: kid.absoluteMaturity,
×
UNCOV
1190
                Stage:          2,
×
UNCOV
1191
        }
×
UNCOV
1192

×
UNCOV
1193
        c.Htlcs = append(c.Htlcs, htlcReport)
×
UNCOV
1194
}
×
1195

1196
// AddLimboStage1SuccessHtlcHtlc adds an htlc crib output to the maturity
1197
// report's set of HTLC's. We'll use this to report any incoming HTLC sweeps
1198
// where the second level transaction hasn't yet confirmed.
1199
func (c *ContractMaturityReport) AddLimboStage1SuccessHtlc(kid *kidOutput) {
×
1200
        c.LimboBalance += kid.Amount()
×
1201

×
1202
        c.Htlcs = append(c.Htlcs, HtlcMaturityReport{
×
1203
                Outpoint: kid.OutPoint(),
×
1204
                Amount:   kid.Amount(),
×
1205
                Stage:    1,
×
1206
        })
×
1207
}
×
1208

1209
// AddLimboStage2Htlc adds an htlc kindergarten output to the maturity report's
1210
// htlcs, and contributes its amount to the limbo balance.
UNCOV
1211
func (c *ContractMaturityReport) AddLimboStage2Htlc(kid *kidOutput) {
×
UNCOV
1212
        c.LimboBalance += kid.Amount()
×
UNCOV
1213

×
UNCOV
1214
        htlcReport := HtlcMaturityReport{
×
UNCOV
1215
                Outpoint: kid.OutPoint(),
×
UNCOV
1216
                Amount:   kid.Amount(),
×
UNCOV
1217
                Stage:    2,
×
UNCOV
1218
        }
×
UNCOV
1219

×
UNCOV
1220
        // If the confirmation height is set, then this means the first stage
×
UNCOV
1221
        // has been confirmed, and we know the final maturity height of the CSV
×
UNCOV
1222
        // delay.
×
UNCOV
1223
        if kid.ConfHeight() != 0 {
×
UNCOV
1224
                htlcReport.MaturityHeight = kid.ConfHeight() + kid.BlocksToMaturity()
×
UNCOV
1225
        }
×
1226

UNCOV
1227
        c.Htlcs = append(c.Htlcs, htlcReport)
×
1228
}
1229

1230
// AddRecoveredHtlc adds a graduate output to the maturity report's htlcs, and
1231
// contributes its amount to the recovered balance.
1232
func (c *ContractMaturityReport) AddRecoveredHtlc(kid *kidOutput) {
×
1233
        c.RecoveredBalance += kid.Amount()
×
1234

×
1235
        c.Htlcs = append(c.Htlcs, HtlcMaturityReport{
×
1236
                Outpoint:       kid.OutPoint(),
×
1237
                Amount:         kid.Amount(),
×
1238
                MaturityHeight: kid.ConfHeight() + kid.BlocksToMaturity(),
×
1239
        })
×
1240
}
×
1241

1242
// closeAndRemoveIfMature removes a particular channel from the channel index
1243
// if and only if all of its outputs have been marked graduated. If the channel
1244
// still has ungraduated outputs, the method will succeed without altering the
1245
// database state.
1246
func (u *UtxoNursery) closeAndRemoveIfMature(chanPoint *wire.OutPoint) error {
3✔
1247
        isMature, err := u.cfg.Store.IsMatureChannel(chanPoint)
3✔
1248
        if err == ErrContractNotFound {
6✔
1249
                return nil
3✔
1250
        } else if err != nil {
3✔
1251
                utxnLog.Errorf("Unable to determine maturity of "+
×
1252
                        "channel=%s", chanPoint)
×
1253
                return err
×
1254
        }
×
1255

1256
        // Nothing to do if we are still incubating.
UNCOV
1257
        if !isMature {
×
1258
                return nil
×
1259
        }
×
1260

1261
        // Now that the channel is fully closed, we remove the channel from the
1262
        // nursery store here. This preserves the invariant that we never remove
1263
        // a channel unless it is mature, as this is the only place the utxo
1264
        // nursery removes a channel.
UNCOV
1265
        if err := u.cfg.Store.RemoveChannel(chanPoint); err != nil {
×
1266
                utxnLog.Errorf("Unable to remove channel=%s from "+
×
1267
                        "nursery store: %v", chanPoint, err)
×
1268
                return err
×
1269
        }
×
1270

UNCOV
1271
        utxnLog.Infof("Removed channel %v from nursery store", chanPoint)
×
UNCOV
1272

×
UNCOV
1273
        return nil
×
1274
}
1275

1276
// babyOutput represents a two-stage CSV locked output, and is used to track
1277
// htlc outputs through incubation. The first stage requires broadcasting a
1278
// presigned timeout txn that spends from the CLTV locked output on the
1279
// commitment txn. A babyOutput is treated as a subset of CsvSpendableOutputs,
1280
// with the additional constraint that a transaction must be broadcast before
1281
// it can be spent. Each baby transaction embeds the kidOutput that can later
1282
// be used to spend the CSV output contained in the timeout txn.
1283
//
1284
// TODO(roasbeef): re-rename to timeout tx
1285
//   - create CltvCsvSpendableOutput
1286
type babyOutput struct {
1287
        // expiry is the absolute block height at which the secondLevelTx
1288
        // should be broadcast to the network.
1289
        //
1290
        // NOTE: This value will be zero if this is a baby output for a prior
1291
        // incoming HTLC.
1292
        expiry uint32
1293

1294
        // timeoutTx is a fully-signed transaction that, upon confirmation,
1295
        // transitions the htlc into the delay+claim stage.
1296
        timeoutTx *wire.MsgTx
1297

1298
        // kidOutput represents the CSV output to be swept from the
1299
        // secondLevelTx after it has been broadcast and confirmed.
1300
        kidOutput
1301
}
1302

1303
// makeBabyOutput constructs a baby output that wraps a future kidOutput. The
1304
// provided sign descriptors and witness types will be used once the output
1305
// reaches the delay and claim stage.
1306
func makeBabyOutput(chanPoint *wire.OutPoint,
1307
        htlcResolution *lnwallet.OutgoingHtlcResolution,
1308
        deadlineHeight fn.Option[int32]) babyOutput {
3✔
1309

3✔
1310
        htlcOutpoint := htlcResolution.ClaimOutpoint
3✔
1311
        blocksToMaturity := htlcResolution.CsvDelay
3✔
1312

3✔
1313
        isTaproot := txscript.IsPayToTaproot(
3✔
1314
                htlcResolution.SweepSignDesc.Output.PkScript,
3✔
1315
        )
3✔
1316

3✔
1317
        var witnessType input.StandardWitnessType
3✔
1318
        if isTaproot {
3✔
1319
                witnessType = input.TaprootHtlcOfferedTimeoutSecondLevel
×
1320
        } else {
3✔
1321
                witnessType = input.HtlcOfferedTimeoutSecondLevel
3✔
1322
        }
3✔
1323

1324
        kid := makeKidOutput(
3✔
1325
                &htlcOutpoint, chanPoint, blocksToMaturity, witnessType,
3✔
1326
                &htlcResolution.SweepSignDesc, 0, deadlineHeight,
3✔
1327
        )
3✔
1328

3✔
1329
        return babyOutput{
3✔
1330
                kidOutput: kid,
3✔
1331
                expiry:    htlcResolution.Expiry,
3✔
1332
                timeoutTx: htlcResolution.SignedTimeoutTx,
3✔
1333
        }
3✔
1334
}
1335

1336
// Encode writes the baby output to the given io.Writer.
1337
func (bo *babyOutput) Encode(w io.Writer) error {
3✔
1338
        var scratch [4]byte
3✔
1339
        byteOrder.PutUint32(scratch[:], bo.expiry)
3✔
1340
        if _, err := w.Write(scratch[:]); err != nil {
3✔
1341
                return err
×
1342
        }
×
1343

1344
        if err := bo.timeoutTx.Serialize(w); err != nil {
3✔
1345
                return err
×
1346
        }
×
1347

1348
        return bo.kidOutput.Encode(w)
3✔
1349
}
1350

1351
// Decode reconstructs a baby output using the provided io.Reader.
1352
func (bo *babyOutput) Decode(r io.Reader) error {
3✔
1353
        var scratch [4]byte
3✔
1354
        if _, err := r.Read(scratch[:]); err != nil {
3✔
1355
                return err
×
1356
        }
×
1357
        bo.expiry = byteOrder.Uint32(scratch[:])
3✔
1358

3✔
1359
        bo.timeoutTx = new(wire.MsgTx)
3✔
1360
        if err := bo.timeoutTx.Deserialize(r); err != nil {
3✔
1361
                return err
×
1362
        }
×
1363

1364
        return bo.kidOutput.Decode(r)
3✔
1365
}
1366

1367
// kidOutput represents an output that's waiting for a required blockheight
1368
// before its funds will be available to be moved into the user's wallet.  The
1369
// struct includes a WitnessGenerator closure which will be used to generate
1370
// the witness required to sweep the output once it's mature.
1371
//
1372
// TODO(roasbeef): rename to immatureOutput?
1373
type kidOutput struct {
1374
        breachedOutput
1375

1376
        originChanPoint wire.OutPoint
1377

1378
        // isHtlc denotes if this kid output is an HTLC output or not. This
1379
        // value will be used to determine how to report this output within the
1380
        // nursery report.
1381
        isHtlc bool
1382

1383
        // blocksToMaturity is the relative CSV delay required after initial
1384
        // confirmation of the commitment transaction before we can sweep this
1385
        // output.
1386
        //
1387
        // NOTE: This will be set for: commitment outputs, and incoming HTLC's.
1388
        // Otherwise, this will be zero. It will also be non-zero for
1389
        // commitment types which requires confirmed spends.
1390
        blocksToMaturity uint32
1391

1392
        // absoluteMaturity is the absolute height that this output will be
1393
        // mature at. In order to sweep the output after this height, the
1394
        // locktime of sweep transaction will need to be set to this value.
1395
        //
1396
        // NOTE: This will only be set for: outgoing HTLC's on the commitment
1397
        // transaction of the remote party.
1398
        absoluteMaturity uint32
1399

1400
        // deadlineHeight is the absolute height that this output should be
1401
        // confirmed at. For an incoming HTLC, this is the CLTV expiry height.
1402
        // For outgoing HTLC, this is its corresponding incoming HTLC's CLTV
1403
        // expiry height.
1404
        deadlineHeight fn.Option[int32]
1405
}
1406

1407
func makeKidOutput(outpoint, originChanPoint *wire.OutPoint,
1408
        blocksToMaturity uint32, witnessType input.StandardWitnessType,
1409
        signDescriptor *input.SignDescriptor, absoluteMaturity uint32,
1410
        deadlineHeight fn.Option[int32]) kidOutput {
3✔
1411

3✔
1412
        // This is an HTLC either if it's an incoming HTLC on our commitment
3✔
1413
        // transaction, or is an outgoing HTLC on the commitment transaction of
3✔
1414
        // the remote peer.
3✔
1415
        isHtlc := (witnessType == input.HtlcAcceptedSuccessSecondLevel ||
3✔
1416
                witnessType == input.TaprootHtlcAcceptedSuccessSecondLevel ||
3✔
1417
                witnessType == input.TaprootHtlcOfferedRemoteTimeout ||
3✔
1418
                witnessType == input.HtlcOfferedRemoteTimeout)
3✔
1419

3✔
1420
        // heightHint can be safely set to zero here, because after this
3✔
1421
        // function returns, nursery will set a proper confirmation height in
3✔
1422
        // waitForTimeoutConf or waitForPreschoolConf.
3✔
1423
        heightHint := uint32(0)
3✔
1424

3✔
1425
        return kidOutput{
3✔
1426
                breachedOutput: makeBreachedOutput(
3✔
1427
                        outpoint, witnessType, nil, signDescriptor, heightHint,
3✔
1428
                        fn.None[tlv.Blob](),
3✔
1429
                ),
3✔
1430
                isHtlc:           isHtlc,
3✔
1431
                originChanPoint:  *originChanPoint,
3✔
1432
                blocksToMaturity: blocksToMaturity,
3✔
1433
                absoluteMaturity: absoluteMaturity,
3✔
1434
                deadlineHeight:   deadlineHeight,
3✔
1435
        }
3✔
1436
}
3✔
1437

1438
func (k *kidOutput) OriginChanPoint() *wire.OutPoint {
3✔
1439
        return &k.originChanPoint
3✔
1440
}
3✔
1441

1442
func (k *kidOutput) BlocksToMaturity() uint32 {
3✔
1443
        return k.blocksToMaturity
3✔
1444
}
3✔
1445

1446
func (k *kidOutput) SetConfHeight(height uint32) {
3✔
1447
        k.confHeight = height
3✔
1448
}
3✔
1449

1450
func (k *kidOutput) ConfHeight() uint32 {
3✔
1451
        return k.confHeight
3✔
1452
}
3✔
1453

1454
func (k *kidOutput) RequiredLockTime() (uint32, bool) {
×
1455
        return k.absoluteMaturity, k.absoluteMaturity > 0
×
1456
}
×
1457

1458
// Encode converts a KidOutput struct into a form suitable for on-disk database
1459
// storage. Note that the signDescriptor struct field is included so that the
1460
// output's witness can be generated by createSweepTx() when the output becomes
1461
// spendable.
1462
func (k *kidOutput) Encode(w io.Writer) error {
3✔
1463
        var scratch [8]byte
3✔
1464
        byteOrder.PutUint64(scratch[:], uint64(k.Amount()))
3✔
1465
        if _, err := w.Write(scratch[:]); err != nil {
3✔
1466
                return err
×
1467
        }
×
1468

1469
        op := k.OutPoint()
3✔
1470
        if err := graphdb.WriteOutpoint(w, &op); err != nil {
3✔
1471
                return err
×
1472
        }
×
1473
        if err := graphdb.WriteOutpoint(w, k.OriginChanPoint()); err != nil {
3✔
1474
                return err
×
1475
        }
×
1476

1477
        if err := binary.Write(w, byteOrder, k.isHtlc); err != nil {
3✔
1478
                return err
×
1479
        }
×
1480

1481
        byteOrder.PutUint32(scratch[:4], k.BlocksToMaturity())
3✔
1482
        if _, err := w.Write(scratch[:4]); err != nil {
3✔
1483
                return err
×
1484
        }
×
1485

1486
        byteOrder.PutUint32(scratch[:4], k.absoluteMaturity)
3✔
1487
        if _, err := w.Write(scratch[:4]); err != nil {
3✔
1488
                return err
×
1489
        }
×
1490

1491
        byteOrder.PutUint32(scratch[:4], k.ConfHeight())
3✔
1492
        if _, err := w.Write(scratch[:4]); err != nil {
3✔
1493
                return err
×
1494
        }
×
1495

1496
        byteOrder.PutUint16(scratch[:2], uint16(k.witnessType))
3✔
1497
        if _, err := w.Write(scratch[:2]); err != nil {
3✔
1498
                return err
×
1499
        }
×
1500

1501
        if err := input.WriteSignDescriptor(w, k.SignDesc()); err != nil {
3✔
1502
                return err
×
1503
        }
×
1504

1505
        if k.SignDesc().ControlBlock == nil {
6✔
1506
                return nil
3✔
1507
        }
3✔
1508

1509
        // If this is a taproot output, then it'll also have a control block,
1510
        // so we'll go ahead and write that now.
1511
        return wire.WriteVarBytes(w, 1000, k.SignDesc().ControlBlock)
×
1512
}
1513

1514
// Decode takes a byte array representation of a kidOutput and converts it to an
1515
// struct. Note that the witnessFunc method isn't added during deserialization
1516
// and must be added later based on the value of the witnessType field.
1517
func (k *kidOutput) Decode(r io.Reader) error {
3✔
1518
        var scratch [8]byte
3✔
1519

3✔
1520
        if _, err := r.Read(scratch[:]); err != nil {
3✔
1521
                return err
×
1522
        }
×
1523
        k.amt = btcutil.Amount(byteOrder.Uint64(scratch[:]))
3✔
1524

3✔
1525
        err := graphdb.ReadOutpoint(io.LimitReader(r, 40), &k.outpoint)
3✔
1526
        if err != nil {
3✔
1527
                return err
×
1528
        }
×
1529

1530
        err = graphdb.ReadOutpoint(io.LimitReader(r, 40), &k.originChanPoint)
3✔
1531
        if err != nil {
3✔
1532
                return err
×
1533
        }
×
1534

1535
        if err := binary.Read(r, byteOrder, &k.isHtlc); err != nil {
3✔
1536
                return err
×
1537
        }
×
1538

1539
        if _, err := r.Read(scratch[:4]); err != nil {
3✔
1540
                return err
×
1541
        }
×
1542
        k.blocksToMaturity = byteOrder.Uint32(scratch[:4])
3✔
1543

3✔
1544
        if _, err := r.Read(scratch[:4]); err != nil {
3✔
1545
                return err
×
1546
        }
×
1547
        k.absoluteMaturity = byteOrder.Uint32(scratch[:4])
3✔
1548

3✔
1549
        if _, err := r.Read(scratch[:4]); err != nil {
3✔
1550
                return err
×
1551
        }
×
1552
        k.confHeight = byteOrder.Uint32(scratch[:4])
3✔
1553

3✔
1554
        if _, err := r.Read(scratch[:2]); err != nil {
3✔
1555
                return err
×
1556
        }
×
1557
        k.witnessType = input.StandardWitnessType(byteOrder.Uint16(scratch[:2]))
3✔
1558

3✔
1559
        if err := input.ReadSignDescriptor(r, &k.signDesc); err != nil {
3✔
1560
                return err
×
1561
        }
×
1562

1563
        // If there's anything left in the reader, then this is a taproot
1564
        // output that also wrote a control block.
1565
        ctrlBlock, err := wire.ReadVarBytes(r, 0, 1000, "control block")
3✔
1566
        switch {
3✔
1567
        // If there're no bytes remaining, then we'll return early.
1568
        case errors.Is(err, io.EOF):
3✔
1569
                fallthrough
3✔
1570
        case errors.Is(err, io.ErrUnexpectedEOF):
3✔
1571
                return nil
3✔
1572

1573
        case err != nil:
×
1574
                return err
×
1575
        }
1576

1577
        k.signDesc.ControlBlock = ctrlBlock
×
1578

×
1579
        return nil
×
1580
}
1581

1582
// Compile-time constraint to ensure kidOutput implements the
1583
// Input interface.
1584

1585
var _ input.Input = (*kidOutput)(nil)
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