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

lightningnetwork / lnd / 12313002221

13 Dec 2024 09:25AM UTC coverage: 57.486% (+8.6%) from 48.92%
12313002221

push

github

web-flow
Merge pull request #9343 from ellemouton/contextGuard

fn: expand the ContextGuard and add tests

101902 of 177264 relevant lines covered (57.49%)

24909.26 hits per line

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

67.05
/contractcourt/commit_sweep_resolver.go
1
package contractcourt
2

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

10
        "github.com/btcsuite/btcd/btcutil"
11
        "github.com/btcsuite/btcd/chaincfg/chainhash"
12
        "github.com/btcsuite/btcd/txscript"
13
        "github.com/btcsuite/btcd/wire"
14
        "github.com/lightningnetwork/lnd/chainntnfs"
15
        "github.com/lightningnetwork/lnd/channeldb"
16
        "github.com/lightningnetwork/lnd/fn/v2"
17
        "github.com/lightningnetwork/lnd/input"
18
        "github.com/lightningnetwork/lnd/lnwallet"
19
        "github.com/lightningnetwork/lnd/sweep"
20
)
21

22
// commitSweepResolver is a resolver that will attempt to sweep the commitment
23
// output paying to us (local channel balance). In the case that the local
24
// party (we) broadcasts their version of the commitment transaction, we have
25
// to wait before sweeping it, as it has a CSV delay. For anchor channel
26
// type, even if the remote party broadcasts the commitment transaction,
27
// we have to wait one block after commitment transaction is confirmed,
28
// because CSV 1 is put into the script of UTXO representing local balance.
29
// Additionally, if the channel is a channel lease, we have to wait for
30
// CLTV to expire.
31
// https://docs.lightning.engineering/lightning-network-tools/pool/overview
32
type commitSweepResolver struct {
33
        // localChanCfg is used to provide the resolver with the keys required
34
        // to identify whether the commitment transaction was broadcast by the
35
        // local or remote party.
36
        localChanCfg channeldb.ChannelConfig
37

38
        // commitResolution contains all data required to successfully sweep
39
        // this HTLC on-chain.
40
        commitResolution lnwallet.CommitOutputResolution
41

42
        // resolved reflects if the contract has been fully resolved or not.
43
        resolved bool
44

45
        // broadcastHeight is the height that the original contract was
46
        // broadcast to the main-chain at. We'll use this value to bound any
47
        // historical queries to the chain for spends/confirmations.
48
        broadcastHeight uint32
49

50
        // chanPoint is the channel point of the original contract.
51
        chanPoint wire.OutPoint
52

53
        // channelInitiator denotes whether the party responsible for resolving
54
        // the contract initiated the channel.
55
        channelInitiator bool
56

57
        // leaseExpiry denotes the additional waiting period the contract must
58
        // hold until it can be resolved. This waiting period is known as the
59
        // expiration of a script-enforced leased channel and only applies to
60
        // the channel initiator.
61
        //
62
        // NOTE: This value should only be set when the contract belongs to a
63
        // leased channel.
64
        leaseExpiry uint32
65

66
        // chanType denotes the type of channel the contract belongs to.
67
        chanType channeldb.ChannelType
68

69
        // currentReport stores the current state of the resolver for reporting
70
        // over the rpc interface.
71
        currentReport ContractReport
72

73
        // reportLock prevents concurrent access to the resolver report.
74
        reportLock sync.Mutex
75

76
        contractResolverKit
77
}
78

79
// newCommitSweepResolver instantiates a new direct commit output resolver.
80
func newCommitSweepResolver(res lnwallet.CommitOutputResolution,
81
        broadcastHeight uint32, chanPoint wire.OutPoint,
82
        resCfg ResolverConfig) *commitSweepResolver {
3✔
83

3✔
84
        r := &commitSweepResolver{
3✔
85
                contractResolverKit: *newContractResolverKit(resCfg),
3✔
86
                commitResolution:    res,
3✔
87
                broadcastHeight:     broadcastHeight,
3✔
88
                chanPoint:           chanPoint,
3✔
89
        }
3✔
90

3✔
91
        r.initLogger(r)
3✔
92
        r.initReport()
3✔
93

3✔
94
        return r
3✔
95
}
3✔
96

97
// ResolverKey returns an identifier which should be globally unique for this
98
// particular resolver within the chain the original contract resides within.
99
func (c *commitSweepResolver) ResolverKey() []byte {
3✔
100
        key := newResolverID(c.commitResolution.SelfOutPoint)
3✔
101
        return key[:]
3✔
102
}
3✔
103

104
// waitForHeight registers for block notifications and waits for the provided
105
// block height to be reached.
106
func waitForHeight(waitHeight uint32, notifier chainntnfs.ChainNotifier,
107
        quit <-chan struct{}) error {
6✔
108

6✔
109
        // Register for block epochs. After registration, the current height
6✔
110
        // will be sent on the channel immediately.
6✔
111
        blockEpochs, err := notifier.RegisterBlockEpochNtfn(nil)
6✔
112
        if err != nil {
6✔
113
                return err
×
114
        }
×
115
        defer blockEpochs.Cancel()
6✔
116

6✔
117
        for {
14✔
118
                select {
8✔
119
                case newBlock, ok := <-blockEpochs.Epochs:
8✔
120
                        if !ok {
8✔
121
                                return errResolverShuttingDown
×
122
                        }
×
123
                        height := newBlock.Height
8✔
124
                        if height >= int32(waitHeight) {
14✔
125
                                return nil
6✔
126
                        }
6✔
127

128
                case <-quit:
×
129
                        return errResolverShuttingDown
×
130
                }
131
        }
132
}
133

134
// waitForSpend waits for the given outpoint to be spent, and returns the
135
// details of the spending tx.
136
func waitForSpend(op *wire.OutPoint, pkScript []byte, heightHint uint32,
137
        notifier chainntnfs.ChainNotifier, quit <-chan struct{}) (
138
        *chainntnfs.SpendDetail, error) {
28✔
139

28✔
140
        spendNtfn, err := notifier.RegisterSpendNtfn(
28✔
141
                op, pkScript, heightHint,
28✔
142
        )
28✔
143
        if err != nil {
28✔
144
                return nil, err
×
145
        }
×
146

147
        select {
28✔
148
        case spendDetail, ok := <-spendNtfn.Spend:
28✔
149
                if !ok {
28✔
150
                        return nil, errResolverShuttingDown
×
151
                }
×
152

153
                return spendDetail, nil
28✔
154

155
        case <-quit:
×
156
                return nil, errResolverShuttingDown
×
157
        }
158
}
159

160
// getCommitTxConfHeight waits for confirmation of the commitment tx and
161
// returns the confirmation height.
162
func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) {
3✔
163
        txID := c.commitResolution.SelfOutPoint.Hash
3✔
164
        signDesc := c.commitResolution.SelfOutputSignDesc
3✔
165
        pkScript := signDesc.Output.PkScript
3✔
166

3✔
167
        const confDepth = 1
3✔
168

3✔
169
        confChan, err := c.Notifier.RegisterConfirmationsNtfn(
3✔
170
                &txID, pkScript, confDepth, c.broadcastHeight,
3✔
171
        )
3✔
172
        if err != nil {
3✔
173
                return 0, err
×
174
        }
×
175
        defer confChan.Cancel()
3✔
176

3✔
177
        select {
3✔
178
        case txConfirmation, ok := <-confChan.Confirmed:
3✔
179
                if !ok {
3✔
180
                        return 0, fmt.Errorf("cannot get confirmation "+
×
181
                                "for commit tx %v", txID)
×
182
                }
×
183

184
                return txConfirmation.BlockHeight, nil
3✔
185

186
        case <-c.quit:
×
187
                return 0, errResolverShuttingDown
×
188
        }
189
}
190

191
// Resolve instructs the contract resolver to resolve the output on-chain. Once
192
// the output has been *fully* resolved, the function should return immediately
193
// with a nil ContractResolver value for the first return value.  In the case
194
// that the contract requires further resolution, then another resolve is
195
// returned.
196
//
197
// NOTE: This function MUST be run as a goroutine.
198
//
199
//nolint:funlen
200
func (c *commitSweepResolver) Resolve(_ bool) (ContractResolver, error) {
3✔
201
        // If we're already resolved, then we can exit early.
3✔
202
        if c.resolved {
3✔
203
                return nil, nil
×
204
        }
×
205

206
        confHeight, err := c.getCommitTxConfHeight()
3✔
207
        if err != nil {
3✔
208
                return nil, err
×
209
        }
×
210

211
        // Wait up until the CSV expires, unless we also have a CLTV that
212
        // expires after.
213
        unlockHeight := confHeight + c.commitResolution.MaturityDelay
3✔
214
        if c.hasCLTV() {
3✔
215
                unlockHeight = uint32(math.Max(
×
216
                        float64(unlockHeight), float64(c.leaseExpiry),
×
217
                ))
×
218
        }
×
219

220
        c.log.Debugf("commit conf_height=%v, unlock_height=%v",
3✔
221
                confHeight, unlockHeight)
3✔
222

3✔
223
        // Update report now that we learned the confirmation height.
3✔
224
        c.reportLock.Lock()
3✔
225
        c.currentReport.MaturityHeight = unlockHeight
3✔
226
        c.reportLock.Unlock()
3✔
227

3✔
228
        // If there is a csv/cltv lock, we'll wait for that.
3✔
229
        if c.commitResolution.MaturityDelay > 0 || c.hasCLTV() {
5✔
230
                // Determine what height we should wait until for the locks to
2✔
231
                // expire.
2✔
232
                var waitHeight uint32
2✔
233
                switch {
2✔
234
                // If we have both a csv and cltv lock, we'll need to look at
235
                // both and see which expires later.
236
                case c.commitResolution.MaturityDelay > 0 && c.hasCLTV():
×
237
                        c.log.Debugf("waiting for CSV and CLTV lock to expire "+
×
238
                                "at height %v", unlockHeight)
×
239
                        // If the CSV expires after the CLTV, or there is no
×
240
                        // CLTV, then we can broadcast a sweep a block before.
×
241
                        // Otherwise, we need to broadcast at our expected
×
242
                        // unlock height.
×
243
                        waitHeight = uint32(math.Max(
×
244
                                float64(unlockHeight-1), float64(c.leaseExpiry),
×
245
                        ))
×
246

247
                // If we only have a csv lock, wait for the height before the
248
                // lock expires as the spend path should be unlocked by then.
249
                case c.commitResolution.MaturityDelay > 0:
2✔
250
                        c.log.Debugf("waiting for CSV lock to expire at "+
2✔
251
                                "height %v", unlockHeight)
2✔
252
                        waitHeight = unlockHeight - 1
2✔
253
                }
254

255
                err := waitForHeight(waitHeight, c.Notifier, c.quit)
2✔
256
                if err != nil {
2✔
257
                        return nil, err
×
258
                }
×
259
        }
260

261
        var (
3✔
262
                isLocalCommitTx bool
3✔
263

3✔
264
                signDesc = c.commitResolution.SelfOutputSignDesc
3✔
265
        )
3✔
266

3✔
267
        switch {
3✔
268
        // For taproot channels, we'll know if this is the local commit based
269
        // on the timelock value. For remote commitment transactions, the
270
        // witness script has a timelock of 1.
271
        case c.chanType.IsTaproot():
×
272
                delayKey := c.localChanCfg.DelayBasePoint.PubKey
×
273
                nonDelayKey := c.localChanCfg.PaymentBasePoint.PubKey
×
274

×
275
                signKey := c.commitResolution.SelfOutputSignDesc.KeyDesc.PubKey
×
276

×
277
                // If the key in the script is neither of these, we shouldn't
×
278
                // proceed. This should be impossible.
×
279
                if !signKey.IsEqual(delayKey) && !signKey.IsEqual(nonDelayKey) {
×
280
                        return nil, fmt.Errorf("unknown sign key %v", signKey)
×
281
                }
×
282

283
                // The commitment transaction is ours iff the signing key is
284
                // the delay key.
285
                isLocalCommitTx = signKey.IsEqual(delayKey)
×
286

287
        // The output is on our local commitment if the script starts with
288
        // OP_IF for the revocation clause. On the remote commitment it will
289
        // either be a regular P2WKH or a simple sig spend with a CSV delay.
290
        default:
3✔
291
                isLocalCommitTx = signDesc.WitnessScript[0] == txscript.OP_IF
3✔
292
        }
293
        isDelayedOutput := c.commitResolution.MaturityDelay != 0
3✔
294

3✔
295
        c.log.Debugf("isDelayedOutput=%v, isLocalCommitTx=%v", isDelayedOutput,
3✔
296
                isLocalCommitTx)
3✔
297

3✔
298
        // There're three types of commitments, those that have tweaks for the
3✔
299
        // remote key (us in this case), those that don't, and a third where
3✔
300
        // there is no tweak and the output is delayed. On the local commitment
3✔
301
        // our output will always be delayed. We'll rely on the presence of the
3✔
302
        // commitment tweak to discern which type of commitment this is.
3✔
303
        var witnessType input.WitnessType
3✔
304
        switch {
3✔
305
        // The local delayed output for a taproot channel.
306
        case isLocalCommitTx && c.chanType.IsTaproot():
×
307
                witnessType = input.TaprootLocalCommitSpend
×
308

309
        // The CSV 1 delayed output for a taproot channel.
310
        case !isLocalCommitTx && c.chanType.IsTaproot():
×
311
                witnessType = input.TaprootRemoteCommitSpend
×
312

313
        // Delayed output to us on our local commitment for a channel lease in
314
        // which we are the initiator.
315
        case isLocalCommitTx && c.hasCLTV():
×
316
                witnessType = input.LeaseCommitmentTimeLock
×
317

318
        // Delayed output to us on our local commitment.
319
        case isLocalCommitTx:
×
320
                witnessType = input.CommitmentTimeLock
×
321

322
        // A confirmed output to us on the remote commitment for a channel lease
323
        // in which we are the initiator.
324
        case isDelayedOutput && c.hasCLTV():
×
325
                witnessType = input.LeaseCommitmentToRemoteConfirmed
×
326

327
        // A confirmed output to us on the remote commitment.
328
        case isDelayedOutput:
2✔
329
                witnessType = input.CommitmentToRemoteConfirmed
2✔
330

331
        // A non-delayed output on the remote commitment where the key is
332
        // tweakless.
333
        case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil:
1✔
334
                witnessType = input.CommitSpendNoDelayTweakless
1✔
335

336
        // A non-delayed output on the remote commitment where the key is
337
        // tweaked.
338
        default:
×
339
                witnessType = input.CommitmentNoDelay
×
340
        }
341

342
        c.log.Infof("Sweeping with witness type: %v", witnessType)
3✔
343

3✔
344
        // We'll craft an input with all the information required for the
3✔
345
        // sweeper to create a fully valid sweeping transaction to recover
3✔
346
        // these coins.
3✔
347
        var inp *input.BaseInput
3✔
348
        if c.hasCLTV() {
3✔
349
                inp = input.NewCsvInputWithCltv(
×
350
                        &c.commitResolution.SelfOutPoint, witnessType,
×
351
                        &c.commitResolution.SelfOutputSignDesc,
×
352
                        c.broadcastHeight, c.commitResolution.MaturityDelay,
×
353
                        c.leaseExpiry,
×
354
                        input.WithResolutionBlob(
×
355
                                c.commitResolution.ResolutionBlob,
×
356
                        ),
×
357
                )
×
358
        } else {
3✔
359
                inp = input.NewCsvInput(
3✔
360
                        &c.commitResolution.SelfOutPoint, witnessType,
3✔
361
                        &c.commitResolution.SelfOutputSignDesc,
3✔
362
                        c.broadcastHeight, c.commitResolution.MaturityDelay,
3✔
363
                        input.WithResolutionBlob(
3✔
364
                                c.commitResolution.ResolutionBlob,
3✔
365
                        ),
3✔
366
                )
3✔
367
        }
3✔
368

369
        // TODO(roasbeef): instead of ading ctrl block to the sign desc, make
370
        // new input type, have sweeper set it?
371

372
        // Calculate the budget for the sweeping this input.
373
        budget := calculateBudget(
3✔
374
                btcutil.Amount(inp.SignDesc().Output.Value),
3✔
375
                c.Budget.ToLocalRatio, c.Budget.ToLocal,
3✔
376
        )
3✔
377
        c.log.Infof("Sweeping commit output using budget=%v", budget)
3✔
378

3✔
379
        // With our input constructed, we'll now offer it to the sweeper.
3✔
380
        resultChan, err := c.Sweeper.SweepInput(
3✔
381
                inp, sweep.Params{
3✔
382
                        Budget: budget,
3✔
383

3✔
384
                        // Specify a nil deadline here as there's no time
3✔
385
                        // pressure.
3✔
386
                        DeadlineHeight: fn.None[int32](),
3✔
387
                },
3✔
388
        )
3✔
389
        if err != nil {
3✔
390
                c.log.Errorf("unable to sweep input: %v", err)
×
391

×
392
                return nil, err
×
393
        }
×
394

395
        var sweepTxID chainhash.Hash
3✔
396

3✔
397
        // Sweeper is going to join this input with other inputs if possible
3✔
398
        // and publish the sweep tx. When the sweep tx confirms, it signals us
3✔
399
        // through the result channel with the outcome. Wait for this to
3✔
400
        // happen.
3✔
401
        outcome := channeldb.ResolverOutcomeClaimed
3✔
402
        select {
3✔
403
        case sweepResult := <-resultChan:
3✔
404
                switch sweepResult.Err {
3✔
405
                // If the remote party was able to sweep this output it's
406
                // likely what we sent was actually a revoked commitment.
407
                // Report the error and continue to wrap up the contract.
408
                case sweep.ErrRemoteSpend:
1✔
409
                        c.log.Warnf("local commitment output was swept by "+
1✔
410
                                "remote party via %v", sweepResult.Tx.TxHash())
1✔
411
                        outcome = channeldb.ResolverOutcomeUnclaimed
1✔
412

413
                // No errors, therefore continue processing.
414
                case nil:
2✔
415
                        c.log.Infof("local commitment output fully resolved by "+
2✔
416
                                "sweep tx: %v", sweepResult.Tx.TxHash())
2✔
417
                // Unknown errors.
418
                default:
×
419
                        c.log.Errorf("unable to sweep input: %v",
×
420
                                sweepResult.Err)
×
421

×
422
                        return nil, sweepResult.Err
×
423
                }
424

425
                sweepTxID = sweepResult.Tx.TxHash()
3✔
426

427
        case <-c.quit:
×
428
                return nil, errResolverShuttingDown
×
429
        }
430

431
        // Funds have been swept and balance is no longer in limbo.
432
        c.reportLock.Lock()
3✔
433
        if outcome == channeldb.ResolverOutcomeClaimed {
5✔
434
                // We only record the balance as recovered if it actually came
2✔
435
                // back to us.
2✔
436
                c.currentReport.RecoveredBalance = c.currentReport.LimboBalance
2✔
437
        }
2✔
438
        c.currentReport.LimboBalance = 0
3✔
439
        c.reportLock.Unlock()
3✔
440
        report := c.currentReport.resolverReport(
3✔
441
                &sweepTxID, channeldb.ResolverTypeCommit, outcome,
3✔
442
        )
3✔
443
        c.resolved = true
3✔
444

3✔
445
        // Checkpoint the resolver with a closure that will write the outcome
3✔
446
        // of the resolver and its sweep transaction to disk.
3✔
447
        return nil, c.Checkpoint(c, report)
3✔
448
}
449

450
// Stop signals the resolver to cancel any current resolution processes, and
451
// suspend.
452
//
453
// NOTE: Part of the ContractResolver interface.
454
func (c *commitSweepResolver) Stop() {
×
455
        close(c.quit)
×
456
}
×
457

458
// IsResolved returns true if the stored state in the resolve is fully
459
// resolved. In this case the target output can be forgotten.
460
//
461
// NOTE: Part of the ContractResolver interface.
462
func (c *commitSweepResolver) IsResolved() bool {
×
463
        return c.resolved
×
464
}
×
465

466
// SupplementState allows the user of a ContractResolver to supplement it with
467
// state required for the proper resolution of a contract.
468
//
469
// NOTE: Part of the ContractResolver interface.
470
func (c *commitSweepResolver) SupplementState(state *channeldb.OpenChannel) {
×
471
        if state.ChanType.HasLeaseExpiration() {
×
472
                c.leaseExpiry = state.ThawHeight
×
473
        }
×
474
        c.localChanCfg = state.LocalChanCfg
×
475
        c.channelInitiator = state.IsInitiator
×
476
        c.chanType = state.ChanType
×
477
}
478

479
// hasCLTV denotes whether the resolver must wait for an additional CLTV to
480
// expire before resolving the contract.
481
func (c *commitSweepResolver) hasCLTV() bool {
11✔
482
        return c.channelInitiator && c.leaseExpiry > 0
11✔
483
}
11✔
484

485
// Encode writes an encoded version of the ContractResolver into the passed
486
// Writer.
487
//
488
// NOTE: Part of the ContractResolver interface.
489
func (c *commitSweepResolver) Encode(w io.Writer) error {
1✔
490
        if err := encodeCommitResolution(w, &c.commitResolution); err != nil {
1✔
491
                return err
×
492
        }
×
493

494
        if err := binary.Write(w, endian, c.resolved); err != nil {
1✔
495
                return err
×
496
        }
×
497
        if err := binary.Write(w, endian, c.broadcastHeight); err != nil {
1✔
498
                return err
×
499
        }
×
500
        if _, err := w.Write(c.chanPoint.Hash[:]); err != nil {
1✔
501
                return err
×
502
        }
×
503
        err := binary.Write(w, endian, c.chanPoint.Index)
1✔
504
        if err != nil {
1✔
505
                return err
×
506
        }
×
507

508
        // Previously a sweep tx was serialized at this point. Refactoring
509
        // removed this, but keep in mind that this data may still be present in
510
        // the database.
511

512
        return nil
1✔
513
}
514

515
// newCommitSweepResolverFromReader attempts to decode an encoded
516
// ContractResolver from the passed Reader instance, returning an active
517
// ContractResolver instance.
518
func newCommitSweepResolverFromReader(r io.Reader, resCfg ResolverConfig) (
519
        *commitSweepResolver, error) {
1✔
520

1✔
521
        c := &commitSweepResolver{
1✔
522
                contractResolverKit: *newContractResolverKit(resCfg),
1✔
523
        }
1✔
524

1✔
525
        if err := decodeCommitResolution(r, &c.commitResolution); err != nil {
1✔
526
                return nil, err
×
527
        }
×
528

529
        if err := binary.Read(r, endian, &c.resolved); err != nil {
1✔
530
                return nil, err
×
531
        }
×
532
        if err := binary.Read(r, endian, &c.broadcastHeight); err != nil {
1✔
533
                return nil, err
×
534
        }
×
535
        _, err := io.ReadFull(r, c.chanPoint.Hash[:])
1✔
536
        if err != nil {
1✔
537
                return nil, err
×
538
        }
×
539
        err = binary.Read(r, endian, &c.chanPoint.Index)
1✔
540
        if err != nil {
1✔
541
                return nil, err
×
542
        }
×
543

544
        // Previously a sweep tx was deserialized at this point. Refactoring
545
        // removed this, but keep in mind that this data may still be present in
546
        // the database.
547

548
        c.initLogger(c)
1✔
549
        c.initReport()
1✔
550

1✔
551
        return c, nil
1✔
552
}
553

554
// report returns a report on the resolution state of the contract.
555
func (c *commitSweepResolver) report() *ContractReport {
6✔
556
        c.reportLock.Lock()
6✔
557
        defer c.reportLock.Unlock()
6✔
558

6✔
559
        cpy := c.currentReport
6✔
560
        return &cpy
6✔
561
}
6✔
562

563
// initReport initializes the pending channels report for this resolver.
564
func (c *commitSweepResolver) initReport() {
4✔
565
        amt := btcutil.Amount(
4✔
566
                c.commitResolution.SelfOutputSignDesc.Output.Value,
4✔
567
        )
4✔
568

4✔
569
        // Set the initial report. All fields are filled in, except for the
4✔
570
        // maturity height which remains 0 until Resolve() is executed.
4✔
571
        //
4✔
572
        // TODO(joostjager): Resolvers only activate after the commit tx
4✔
573
        // confirms. With more refactoring in channel arbitrator, it would be
4✔
574
        // possible to make the confirmation height part of ResolverConfig and
4✔
575
        // populate MaturityHeight here.
4✔
576
        c.currentReport = ContractReport{
4✔
577
                Outpoint:         c.commitResolution.SelfOutPoint,
4✔
578
                Type:             ReportOutputUnencumbered,
4✔
579
                Amount:           amt,
4✔
580
                LimboBalance:     amt,
4✔
581
                RecoveredBalance: 0,
4✔
582
        }
4✔
583
}
4✔
584

585
// A compile time assertion to ensure commitSweepResolver meets the
586
// ContractResolver interface.
587
var _ reportingContractResolver = (*commitSweepResolver)(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