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

lightningnetwork / lnd / 12426855566

20 Dec 2024 06:42AM UTC coverage: 58.73% (+0.09%) from 58.64%
12426855566

Pull #9315

github

yyforyongyu
contractcourt: include custom records on replayed htlc

Add another case in addition to #9357.
Pull Request #9315: Implement `blockbeat`

2262 of 2729 new or added lines in 35 files covered. (82.89%)

132 existing lines in 25 files now uncovered.

135298 of 230373 relevant lines covered (58.73%)

19195.08 hits per line

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

82.39
/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
        // broadcastHeight is the height that the original contract was
43
        // broadcast to the main-chain at. We'll use this value to bound any
44
        // historical queries to the chain for spends/confirmations.
45
        broadcastHeight uint32
46

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

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

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

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

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

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

73
        contractResolverKit
74
}
75

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

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

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

7✔
91
        return r
7✔
92
}
7✔
93

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

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

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

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

120
                return spendDetail, nil
34✔
121

122
        case <-quit:
4✔
123
                return nil, errResolverShuttingDown
4✔
124
        }
125
}
126

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

7✔
134
        const confDepth = 1
7✔
135

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

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

151
                return txConfirmation.BlockHeight, nil
7✔
152

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

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

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

176
        var sweepTxID chainhash.Hash
7✔
177

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

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

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

206
                sweepTxID = sweepResult.Tx.TxHash()
7✔
207

208
        case <-c.quit:
4✔
209
                return nil, errResolverShuttingDown
4✔
210
        }
211

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

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

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

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

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

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

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

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

287
        return nil
5✔
288
}
289

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

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

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

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

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

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

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

5✔
331
        return c, nil
5✔
332
}
333

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

10✔
339
        cpy := c.currentReport
10✔
340
        return &cpy
10✔
341
}
10✔
342

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

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

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

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

376
        c.log.Debugf("launching resolver...")
7✔
377
        c.markLaunched()
7✔
378

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

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

390
        // Wait up until the CSV expires, unless we also have a CLTV that
391
        // expires after.
392
        unlockHeight := confHeight + c.commitResolution.MaturityDelay
7✔
393
        if c.hasCLTV() {
11✔
394
                unlockHeight = uint32(math.Max(
4✔
395
                        float64(unlockHeight), float64(c.leaseExpiry),
4✔
396
                ))
4✔
397
        }
4✔
398

399
        // Update report now that we learned the confirmation height.
400
        c.reportLock.Lock()
7✔
401
        c.currentReport.MaturityHeight = unlockHeight
7✔
402
        c.reportLock.Unlock()
7✔
403

7✔
404
        // Derive the witness type for this input.
7✔
405
        witnessType, err := c.decideWitnessType()
7✔
406
        if err != nil {
7✔
NEW
407
                return err
×
NEW
408
        }
×
409

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

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

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

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

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

×
NEW
453
                return err
×
NEW
454
        }
×
455

456
        c.sweepResultChan = resultChan
7✔
457

7✔
458
        return nil
7✔
459
}
460

461
// decideWitnessType returns the witness type for the input.
462
func (c *commitSweepResolver) decideWitnessType() (input.WitnessType, error) {
7✔
463
        var (
7✔
464
                isLocalCommitTx bool
7✔
465
                signDesc        = c.commitResolution.SelfOutputSignDesc
7✔
466
        )
7✔
467

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

4✔
476
                signKey := c.commitResolution.SelfOutputSignDesc.KeyDesc.PubKey
4✔
477

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

484
                // The commitment transaction is ours iff the signing key is
485
                // the delay key.
486
                isLocalCommitTx = signKey.IsEqual(delayKey)
4✔
487

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

495
        isDelayedOutput := c.commitResolution.MaturityDelay != 0
7✔
496

7✔
497
        c.log.Debugf("isDelayedOutput=%v, isLocalCommitTx=%v", isDelayedOutput,
7✔
498
                isLocalCommitTx)
7✔
499

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

511
        // The CSV 1 delayed output for a taproot channel.
512
        case !isLocalCommitTx && c.chanType.IsTaproot():
4✔
513
                witnessType = input.TaprootRemoteCommitSpend
4✔
514

515
        // Delayed output to us on our local commitment for a channel lease in
516
        // which we are the initiator.
517
        case isLocalCommitTx && c.hasCLTV():
4✔
518
                witnessType = input.LeaseCommitmentTimeLock
4✔
519

520
        // Delayed output to us on our local commitment.
521
        case isLocalCommitTx:
4✔
522
                witnessType = input.CommitmentTimeLock
4✔
523

524
        // A confirmed output to us on the remote commitment for a channel lease
525
        // in which we are the initiator.
526
        case isDelayedOutput && c.hasCLTV():
4✔
527
                witnessType = input.LeaseCommitmentToRemoteConfirmed
4✔
528

529
        // A confirmed output to us on the remote commitment.
530
        case isDelayedOutput:
6✔
531
                witnessType = input.CommitmentToRemoteConfirmed
6✔
532

533
        // A non-delayed output on the remote commitment where the key is
534
        // tweakless.
535
        case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil:
5✔
536
                witnessType = input.CommitSpendNoDelayTweakless
5✔
537

538
        // A non-delayed output on the remote commitment where the key is
539
        // tweaked.
NEW
540
        default:
×
NEW
541
                witnessType = input.CommitmentNoDelay
×
542
        }
543

544
        return witnessType, nil
7✔
545
}
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