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

lightningnetwork / lnd / 13440912774

20 Feb 2025 05:14PM UTC coverage: 57.697% (-1.1%) from 58.802%
13440912774

Pull #9535

github

guggero
GitHub: remove duplicate caching

Turns out that actions/setup-go starting with @v4 also adds caching.
With that, our cache size on disk has almost doubled, leading to the
GitHub runner running out of space in certain situation.
We fix that by disabling the automated caching since we already have our
own, custom-tailored version.
Pull Request #9535: GitHub: remove duplicate caching

103519 of 179417 relevant lines covered (57.7%)

24825.3 hits per line

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

67.41
/contractcourt/commit_sweep_resolver.go
1
package contractcourt
2

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

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

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

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

41
        // broadcastHeight is the height that the original contract was
42
        // broadcast to the main-chain at. We'll use this value to bound any
43
        // historical queries to the chain for spends/confirmations.
44
        broadcastHeight uint32
45

46
        // chanPoint is the channel point of the original contract.
47
        chanPoint wire.OutPoint
48

49
        // channelInitiator denotes whether the party responsible for resolving
50
        // the contract initiated the channel.
51
        channelInitiator bool
52

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

62
        // chanType denotes the type of channel the contract belongs to.
63
        chanType channeldb.ChannelType
64

65
        // currentReport stores the current state of the resolver for reporting
66
        // over the rpc interface.
67
        currentReport ContractReport
68

69
        // reportLock prevents concurrent access to the resolver report.
70
        reportLock sync.Mutex
71

72
        contractResolverKit
73
}
74

75
// newCommitSweepResolver instantiates a new direct commit output resolver.
76
func newCommitSweepResolver(res lnwallet.CommitOutputResolution,
77
        broadcastHeight uint32, chanPoint wire.OutPoint,
78
        resCfg ResolverConfig) *commitSweepResolver {
3✔
79

3✔
80
        r := &commitSweepResolver{
3✔
81
                contractResolverKit: *newContractResolverKit(resCfg),
3✔
82
                commitResolution:    res,
3✔
83
                broadcastHeight:     broadcastHeight,
3✔
84
                chanPoint:           chanPoint,
3✔
85
        }
3✔
86

3✔
87
        r.initLogger(fmt.Sprintf("%T(%v)", r, r.commitResolution.SelfOutPoint))
3✔
88
        r.initReport()
3✔
89

3✔
90
        return r
3✔
91
}
3✔
92

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

100
// waitForSpend waits for the given outpoint to be spent, and returns the
101
// details of the spending tx.
102
func waitForSpend(op *wire.OutPoint, pkScript []byte, heightHint uint32,
103
        notifier chainntnfs.ChainNotifier, quit <-chan struct{}) (
104
        *chainntnfs.SpendDetail, error) {
30✔
105

30✔
106
        spendNtfn, err := notifier.RegisterSpendNtfn(
30✔
107
                op, pkScript, heightHint,
30✔
108
        )
30✔
109
        if err != nil {
30✔
110
                return nil, err
×
111
        }
×
112

113
        select {
30✔
114
        case spendDetail, ok := <-spendNtfn.Spend:
30✔
115
                if !ok {
30✔
116
                        return nil, errResolverShuttingDown
×
117
                }
×
118

119
                return spendDetail, nil
30✔
120

121
        case <-quit:
×
122
                return nil, errResolverShuttingDown
×
123
        }
124
}
125

126
// getCommitTxConfHeight waits for confirmation of the commitment tx and
127
// returns the confirmation height.
128
func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) {
3✔
129
        txID := c.commitResolution.SelfOutPoint.Hash
3✔
130
        signDesc := c.commitResolution.SelfOutputSignDesc
3✔
131
        pkScript := signDesc.Output.PkScript
3✔
132

3✔
133
        const confDepth = 1
3✔
134

3✔
135
        confChan, err := c.Notifier.RegisterConfirmationsNtfn(
3✔
136
                &txID, pkScript, confDepth, c.broadcastHeight,
3✔
137
        )
3✔
138
        if err != nil {
3✔
139
                return 0, err
×
140
        }
×
141
        defer confChan.Cancel()
3✔
142

3✔
143
        select {
3✔
144
        case txConfirmation, ok := <-confChan.Confirmed:
3✔
145
                if !ok {
3✔
146
                        return 0, fmt.Errorf("cannot get confirmation "+
×
147
                                "for commit tx %v", txID)
×
148
                }
×
149

150
                return txConfirmation.BlockHeight, nil
3✔
151

152
        case <-c.quit:
×
153
                return 0, errResolverShuttingDown
×
154
        }
155
}
156

157
// Resolve instructs the contract resolver to resolve the output on-chain. Once
158
// the output has been *fully* resolved, the function should return immediately
159
// with a nil ContractResolver value for the first return value.  In the case
160
// that the contract requires further resolution, then another resolve is
161
// returned.
162
//
163
// NOTE: This function MUST be run as a goroutine.
164

165
// TODO(yy): fix the funlen in the next PR.
166
//
167
//nolint:funlen
168
func (c *commitSweepResolver) Resolve() (ContractResolver, error) {
3✔
169
        // If we're already resolved, then we can exit early.
3✔
170
        if c.IsResolved() {
3✔
171
                c.log.Errorf("already resolved")
×
172
                return nil, nil
×
173
        }
×
174

175
        var sweepTxID chainhash.Hash
3✔
176

3✔
177
        // Sweeper is going to join this input with other inputs if possible
3✔
178
        // and publish the sweep tx. When the sweep tx confirms, it signals us
3✔
179
        // through the result channel with the outcome. Wait for this to
3✔
180
        // happen.
3✔
181
        outcome := channeldb.ResolverOutcomeClaimed
3✔
182
        select {
3✔
183
        case sweepResult := <-c.sweepResultChan:
3✔
184
                switch sweepResult.Err {
3✔
185
                // If the remote party was able to sweep this output it's
186
                // likely what we sent was actually a revoked commitment.
187
                // Report the error and continue to wrap up the contract.
188
                case sweep.ErrRemoteSpend:
1✔
189
                        c.log.Warnf("local commitment output was swept by "+
1✔
190
                                "remote party via %v", sweepResult.Tx.TxHash())
1✔
191
                        outcome = channeldb.ResolverOutcomeUnclaimed
1✔
192

193
                // No errors, therefore continue processing.
194
                case nil:
2✔
195
                        c.log.Infof("local commitment output fully resolved by "+
2✔
196
                                "sweep tx: %v", sweepResult.Tx.TxHash())
2✔
197
                // Unknown errors.
198
                default:
×
199
                        c.log.Errorf("unable to sweep input: %v",
×
200
                                sweepResult.Err)
×
201

×
202
                        return nil, sweepResult.Err
×
203
                }
204

205
                sweepTxID = sweepResult.Tx.TxHash()
3✔
206

207
        case <-c.quit:
×
208
                return nil, errResolverShuttingDown
×
209
        }
210

211
        // Funds have been swept and balance is no longer in limbo.
212
        c.reportLock.Lock()
3✔
213
        if outcome == channeldb.ResolverOutcomeClaimed {
5✔
214
                // We only record the balance as recovered if it actually came
2✔
215
                // back to us.
2✔
216
                c.currentReport.RecoveredBalance = c.currentReport.LimboBalance
2✔
217
        }
2✔
218
        c.currentReport.LimboBalance = 0
3✔
219
        c.reportLock.Unlock()
3✔
220
        report := c.currentReport.resolverReport(
3✔
221
                &sweepTxID, channeldb.ResolverTypeCommit, outcome,
3✔
222
        )
3✔
223
        c.markResolved()
3✔
224

3✔
225
        // Checkpoint the resolver with a closure that will write the outcome
3✔
226
        // of the resolver and its sweep transaction to disk.
3✔
227
        return nil, c.Checkpoint(c, report)
3✔
228
}
229

230
// Stop signals the resolver to cancel any current resolution processes, and
231
// suspend.
232
//
233
// NOTE: Part of the ContractResolver interface.
234
func (c *commitSweepResolver) Stop() {
×
235
        c.log.Debugf("stopping...")
×
236
        defer c.log.Debugf("stopped")
×
237
        close(c.quit)
×
238
}
×
239

240
// SupplementState allows the user of a ContractResolver to supplement it with
241
// state required for the proper resolution of a contract.
242
//
243
// NOTE: Part of the ContractResolver interface.
244
func (c *commitSweepResolver) SupplementState(state *channeldb.OpenChannel) {
×
245
        if state.ChanType.HasLeaseExpiration() {
×
246
                c.leaseExpiry = state.ThawHeight
×
247
        }
×
248
        c.localChanCfg = state.LocalChanCfg
×
249
        c.channelInitiator = state.IsInitiator
×
250
        c.chanType = state.ChanType
×
251
}
252

253
// hasCLTV denotes whether the resolver must wait for an additional CLTV to
254
// expire before resolving the contract.
255
func (c *commitSweepResolver) hasCLTV() bool {
8✔
256
        return c.channelInitiator && c.leaseExpiry > 0
8✔
257
}
8✔
258

259
// Encode writes an encoded version of the ContractResolver into the passed
260
// Writer.
261
//
262
// NOTE: Part of the ContractResolver interface.
263
func (c *commitSweepResolver) Encode(w io.Writer) error {
1✔
264
        if err := encodeCommitResolution(w, &c.commitResolution); err != nil {
1✔
265
                return err
×
266
        }
×
267

268
        if err := binary.Write(w, endian, c.IsResolved()); err != nil {
1✔
269
                return err
×
270
        }
×
271
        if err := binary.Write(w, endian, c.broadcastHeight); err != nil {
1✔
272
                return err
×
273
        }
×
274
        if _, err := w.Write(c.chanPoint.Hash[:]); err != nil {
1✔
275
                return err
×
276
        }
×
277
        err := binary.Write(w, endian, c.chanPoint.Index)
1✔
278
        if err != nil {
1✔
279
                return err
×
280
        }
×
281

282
        // Previously a sweep tx was serialized at this point. Refactoring
283
        // removed this, but keep in mind that this data may still be present in
284
        // the database.
285

286
        return nil
1✔
287
}
288

289
// newCommitSweepResolverFromReader attempts to decode an encoded
290
// ContractResolver from the passed Reader instance, returning an active
291
// ContractResolver instance.
292
func newCommitSweepResolverFromReader(r io.Reader, resCfg ResolverConfig) (
293
        *commitSweepResolver, error) {
1✔
294

1✔
295
        c := &commitSweepResolver{
1✔
296
                contractResolverKit: *newContractResolverKit(resCfg),
1✔
297
        }
1✔
298

1✔
299
        if err := decodeCommitResolution(r, &c.commitResolution); err != nil {
1✔
300
                return nil, err
×
301
        }
×
302

303
        var resolved bool
1✔
304
        if err := binary.Read(r, endian, &resolved); err != nil {
1✔
305
                return nil, err
×
306
        }
×
307
        if resolved {
1✔
308
                c.markResolved()
×
309
        }
×
310

311
        if err := binary.Read(r, endian, &c.broadcastHeight); err != nil {
1✔
312
                return nil, err
×
313
        }
×
314
        _, err := io.ReadFull(r, c.chanPoint.Hash[:])
1✔
315
        if err != nil {
1✔
316
                return nil, err
×
317
        }
×
318
        err = binary.Read(r, endian, &c.chanPoint.Index)
1✔
319
        if err != nil {
1✔
320
                return nil, err
×
321
        }
×
322

323
        // Previously a sweep tx was deserialized at this point. Refactoring
324
        // removed this, but keep in mind that this data may still be present in
325
        // the database.
326

327
        c.initLogger(fmt.Sprintf("%T(%v)", c, c.commitResolution.SelfOutPoint))
1✔
328
        c.initReport()
1✔
329

1✔
330
        return c, nil
1✔
331
}
332

333
// report returns a report on the resolution state of the contract.
334
func (c *commitSweepResolver) report() *ContractReport {
6✔
335
        c.reportLock.Lock()
6✔
336
        defer c.reportLock.Unlock()
6✔
337

6✔
338
        cpy := c.currentReport
6✔
339
        return &cpy
6✔
340
}
6✔
341

342
// initReport initializes the pending channels report for this resolver.
343
func (c *commitSweepResolver) initReport() {
4✔
344
        amt := btcutil.Amount(
4✔
345
                c.commitResolution.SelfOutputSignDesc.Output.Value,
4✔
346
        )
4✔
347

4✔
348
        // Set the initial report. All fields are filled in, except for the
4✔
349
        // maturity height which remains 0 until Resolve() is executed.
4✔
350
        //
4✔
351
        // TODO(joostjager): Resolvers only activate after the commit tx
4✔
352
        // confirms. With more refactoring in channel arbitrator, it would be
4✔
353
        // possible to make the confirmation height part of ResolverConfig and
4✔
354
        // populate MaturityHeight here.
4✔
355
        c.currentReport = ContractReport{
4✔
356
                Outpoint:         c.commitResolution.SelfOutPoint,
4✔
357
                Type:             ReportOutputUnencumbered,
4✔
358
                Amount:           amt,
4✔
359
                LimboBalance:     amt,
4✔
360
                RecoveredBalance: 0,
4✔
361
        }
4✔
362
}
4✔
363

364
// A compile time assertion to ensure commitSweepResolver meets the
365
// ContractResolver interface.
366
var _ reportingContractResolver = (*commitSweepResolver)(nil)
367

368
// Launch constructs a commit input and offers it to the sweeper.
369
func (c *commitSweepResolver) Launch() error {
3✔
370
        if c.isLaunched() {
3✔
371
                c.log.Tracef("already launched")
×
372
                return nil
×
373
        }
×
374

375
        c.log.Debugf("launching resolver...")
3✔
376
        c.markLaunched()
3✔
377

3✔
378
        // If we're already resolved, then we can exit early.
3✔
379
        if c.IsResolved() {
3✔
380
                c.log.Errorf("already resolved")
×
381
                return nil
×
382
        }
×
383

384
        confHeight, err := c.getCommitTxConfHeight()
3✔
385
        if err != nil {
3✔
386
                return err
×
387
        }
×
388

389
        // Wait up until the CSV expires, unless we also have a CLTV that
390
        // expires after.
391
        unlockHeight := confHeight + c.commitResolution.MaturityDelay
3✔
392
        if c.hasCLTV() {
3✔
393
                unlockHeight = max(unlockHeight, c.leaseExpiry)
×
394
        }
×
395

396
        // Update report now that we learned the confirmation height.
397
        c.reportLock.Lock()
3✔
398
        c.currentReport.MaturityHeight = unlockHeight
3✔
399
        c.reportLock.Unlock()
3✔
400

3✔
401
        // Derive the witness type for this input.
3✔
402
        witnessType, err := c.decideWitnessType()
3✔
403
        if err != nil {
3✔
404
                return err
×
405
        }
×
406

407
        // We'll craft an input with all the information required for the
408
        // sweeper to create a fully valid sweeping transaction to recover
409
        // these coins.
410
        var inp *input.BaseInput
3✔
411
        if c.hasCLTV() {
3✔
412
                inp = input.NewCsvInputWithCltv(
×
413
                        &c.commitResolution.SelfOutPoint, witnessType,
×
414
                        &c.commitResolution.SelfOutputSignDesc,
×
415
                        c.broadcastHeight, c.commitResolution.MaturityDelay,
×
416
                        c.leaseExpiry,
×
417
                )
×
418
        } else {
3✔
419
                inp = input.NewCsvInput(
3✔
420
                        &c.commitResolution.SelfOutPoint, witnessType,
3✔
421
                        &c.commitResolution.SelfOutputSignDesc,
3✔
422
                        c.broadcastHeight, c.commitResolution.MaturityDelay,
3✔
423
                )
3✔
424
        }
3✔
425

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

429
        // Calculate the budget for the sweeping this input.
430
        budget := calculateBudget(
3✔
431
                btcutil.Amount(inp.SignDesc().Output.Value),
3✔
432
                c.Budget.ToLocalRatio, c.Budget.ToLocal,
3✔
433
        )
3✔
434
        c.log.Infof("sweeping commit output %v using budget=%v", witnessType,
3✔
435
                budget)
3✔
436

3✔
437
        // With our input constructed, we'll now offer it to the sweeper.
3✔
438
        resultChan, err := c.Sweeper.SweepInput(
3✔
439
                inp, sweep.Params{
3✔
440
                        Budget: budget,
3✔
441

3✔
442
                        // Specify a nil deadline here as there's no time
3✔
443
                        // pressure.
3✔
444
                        DeadlineHeight: fn.None[int32](),
3✔
445
                },
3✔
446
        )
3✔
447
        if err != nil {
3✔
448
                c.log.Errorf("unable to sweep input: %v", err)
×
449

×
450
                return err
×
451
        }
×
452

453
        c.sweepResultChan = resultChan
3✔
454

3✔
455
        return nil
3✔
456
}
457

458
// decideWitnessType returns the witness type for the input.
459
func (c *commitSweepResolver) decideWitnessType() (input.WitnessType, error) {
3✔
460
        var (
3✔
461
                isLocalCommitTx bool
3✔
462
                signDesc        = c.commitResolution.SelfOutputSignDesc
3✔
463
        )
3✔
464

3✔
465
        switch {
3✔
466
        // For taproot channels, we'll know if this is the local commit based
467
        // on the timelock value. For remote commitment transactions, the
468
        // witness script has a timelock of 1.
469
        case c.chanType.IsTaproot():
×
470
                delayKey := c.localChanCfg.DelayBasePoint.PubKey
×
471
                nonDelayKey := c.localChanCfg.PaymentBasePoint.PubKey
×
472

×
473
                signKey := c.commitResolution.SelfOutputSignDesc.KeyDesc.PubKey
×
474

×
475
                // If the key in the script is neither of these, we shouldn't
×
476
                // proceed. This should be impossible.
×
477
                if !signKey.IsEqual(delayKey) && !signKey.IsEqual(nonDelayKey) {
×
478
                        return nil, fmt.Errorf("unknown sign key %v", signKey)
×
479
                }
×
480

481
                // The commitment transaction is ours iff the signing key is
482
                // the delay key.
483
                isLocalCommitTx = signKey.IsEqual(delayKey)
×
484

485
        // The output is on our local commitment if the script starts with
486
        // OP_IF for the revocation clause. On the remote commitment it will
487
        // either be a regular P2WKH or a simple sig spend with a CSV delay.
488
        default:
3✔
489
                isLocalCommitTx = signDesc.WitnessScript[0] == txscript.OP_IF
3✔
490
        }
491

492
        isDelayedOutput := c.commitResolution.MaturityDelay != 0
3✔
493

3✔
494
        c.log.Debugf("isDelayedOutput=%v, isLocalCommitTx=%v", isDelayedOutput,
3✔
495
                isLocalCommitTx)
3✔
496

3✔
497
        // There're three types of commitments, those that have tweaks for the
3✔
498
        // remote key (us in this case), those that don't, and a third where
3✔
499
        // there is no tweak and the output is delayed. On the local commitment
3✔
500
        // our output will always be delayed. We'll rely on the presence of the
3✔
501
        // commitment tweak to discern which type of commitment this is.
3✔
502
        var witnessType input.WitnessType
3✔
503
        switch {
3✔
504
        // The local delayed output for a taproot channel.
505
        case isLocalCommitTx && c.chanType.IsTaproot():
×
506
                witnessType = input.TaprootLocalCommitSpend
×
507

508
        // The CSV 1 delayed output for a taproot channel.
509
        case !isLocalCommitTx && c.chanType.IsTaproot():
×
510
                witnessType = input.TaprootRemoteCommitSpend
×
511

512
        // Delayed output to us on our local commitment for a channel lease in
513
        // which we are the initiator.
514
        case isLocalCommitTx && c.hasCLTV():
×
515
                witnessType = input.LeaseCommitmentTimeLock
×
516

517
        // Delayed output to us on our local commitment.
518
        case isLocalCommitTx:
×
519
                witnessType = input.CommitmentTimeLock
×
520

521
        // A confirmed output to us on the remote commitment for a channel lease
522
        // in which we are the initiator.
523
        case isDelayedOutput && c.hasCLTV():
×
524
                witnessType = input.LeaseCommitmentToRemoteConfirmed
×
525

526
        // A confirmed output to us on the remote commitment.
527
        case isDelayedOutput:
2✔
528
                witnessType = input.CommitmentToRemoteConfirmed
2✔
529

530
        // A non-delayed output on the remote commitment where the key is
531
        // tweakless.
532
        case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil:
1✔
533
                witnessType = input.CommitSpendNoDelayTweakless
1✔
534

535
        // A non-delayed output on the remote commitment where the key is
536
        // tweaked.
537
        default:
×
538
                witnessType = input.CommitmentNoDelay
×
539
        }
540

541
        return witnessType, nil
3✔
542
}
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