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

lightningnetwork / lnd / 19924300449

04 Dec 2025 09:35AM UTC coverage: 53.479% (-1.9%) from 55.404%
19924300449

Pull #10419

github

web-flow
Merge f811805c6 into 20473482d
Pull Request #10419: [docs] Document use-native-sql=true for SQL migration step 2

110496 of 206616 relevant lines covered (53.48%)

21221.61 hits per line

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

67.24
/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
        // confirmHeight is the block height that the commitment transaction was
42
        // confirmed at. We'll use this value to bound any historical queries to
43
        // the chain for spends/confirmations.
44
        confirmHeight 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
        confirmHeight 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
                confirmHeight:       confirmHeight,
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
// Resolve instructs the contract resolver to resolve the output on-chain. Once
127
// the output has been *fully* resolved, the function should return immediately
128
// with a nil ContractResolver value for the first return value.  In the case
129
// that the contract requires further resolution, then another resolve is
130
// returned.
131
//
132
// NOTE: This function MUST be run as a goroutine.
133

134
// TODO(yy): fix the funlen in the next PR.
135
//
136
//nolint:funlen
137
func (c *commitSweepResolver) Resolve() (ContractResolver, error) {
3✔
138
        // If we're already resolved, then we can exit early.
3✔
139
        if c.IsResolved() {
3✔
140
                c.log.Errorf("already resolved")
×
141
                return nil, nil
×
142
        }
×
143

144
        var sweepTxID chainhash.Hash
3✔
145

3✔
146
        // Sweeper is going to join this input with other inputs if possible
3✔
147
        // and publish the sweep tx. When the sweep tx confirms, it signals us
3✔
148
        // through the result channel with the outcome. Wait for this to
3✔
149
        // happen.
3✔
150
        outcome := channeldb.ResolverOutcomeClaimed
3✔
151
        select {
3✔
152
        case sweepResult := <-c.sweepResultChan:
3✔
153
                switch sweepResult.Err {
3✔
154
                // If the remote party was able to sweep this output it's
155
                // likely what we sent was actually a revoked commitment.
156
                // Report the error and continue to wrap up the contract.
157
                case sweep.ErrRemoteSpend:
1✔
158
                        c.log.Warnf("local commitment output was swept by "+
1✔
159
                                "remote party via %v", sweepResult.Tx.TxHash())
1✔
160
                        outcome = channeldb.ResolverOutcomeUnclaimed
1✔
161

162
                // No errors, therefore continue processing.
163
                case nil:
2✔
164
                        c.log.Infof("local commitment output fully resolved by "+
2✔
165
                                "sweep tx: %v", sweepResult.Tx.TxHash())
2✔
166
                // Unknown errors.
167
                default:
×
168
                        c.log.Errorf("unable to sweep input: %v",
×
169
                                sweepResult.Err)
×
170

×
171
                        return nil, sweepResult.Err
×
172
                }
173

174
                sweepTxID = sweepResult.Tx.TxHash()
3✔
175

176
        case <-c.quit:
×
177
                return nil, errResolverShuttingDown
×
178
        }
179

180
        // Funds have been swept and balance is no longer in limbo.
181
        c.reportLock.Lock()
3✔
182
        if outcome == channeldb.ResolverOutcomeClaimed {
5✔
183
                // We only record the balance as recovered if it actually came
2✔
184
                // back to us.
2✔
185
                c.currentReport.RecoveredBalance = c.currentReport.LimboBalance
2✔
186
        }
2✔
187
        c.currentReport.LimboBalance = 0
3✔
188
        c.reportLock.Unlock()
3✔
189
        report := c.currentReport.resolverReport(
3✔
190
                &sweepTxID, channeldb.ResolverTypeCommit, outcome,
3✔
191
        )
3✔
192
        c.markResolved()
3✔
193

3✔
194
        // Checkpoint the resolver with a closure that will write the outcome
3✔
195
        // of the resolver and its sweep transaction to disk.
3✔
196
        return nil, c.Checkpoint(c, report)
3✔
197
}
198

199
// Stop signals the resolver to cancel any current resolution processes, and
200
// suspend.
201
//
202
// NOTE: Part of the ContractResolver interface.
203
func (c *commitSweepResolver) Stop() {
×
204
        c.log.Debugf("stopping...")
×
205
        defer c.log.Debugf("stopped")
×
206
        close(c.quit)
×
207
}
×
208

209
// SupplementState allows the user of a ContractResolver to supplement it with
210
// state required for the proper resolution of a contract.
211
//
212
// NOTE: Part of the ContractResolver interface.
213
func (c *commitSweepResolver) SupplementState(state *channeldb.OpenChannel) {
×
214
        if state.ChanType.HasLeaseExpiration() {
×
215
                c.leaseExpiry = state.ThawHeight
×
216
        }
×
217
        c.localChanCfg = state.LocalChanCfg
×
218
        c.channelInitiator = state.IsInitiator
×
219
        c.chanType = state.ChanType
×
220
}
221

222
// hasCLTV denotes whether the resolver must wait for an additional CLTV to
223
// expire before resolving the contract.
224
func (c *commitSweepResolver) hasCLTV() bool {
8✔
225
        return c.channelInitiator && c.leaseExpiry > 0
8✔
226
}
8✔
227

228
// Encode writes an encoded version of the ContractResolver into the passed
229
// Writer.
230
//
231
// NOTE: Part of the ContractResolver interface.
232
func (c *commitSweepResolver) Encode(w io.Writer) error {
1✔
233
        if err := encodeCommitResolution(w, &c.commitResolution); err != nil {
1✔
234
                return err
×
235
        }
×
236

237
        if err := binary.Write(w, endian, c.IsResolved()); err != nil {
1✔
238
                return err
×
239
        }
×
240
        if err := binary.Write(w, endian, c.confirmHeight); err != nil {
1✔
241
                return err
×
242
        }
×
243
        if _, err := w.Write(c.chanPoint.Hash[:]); err != nil {
1✔
244
                return err
×
245
        }
×
246
        err := binary.Write(w, endian, c.chanPoint.Index)
1✔
247
        if err != nil {
1✔
248
                return err
×
249
        }
×
250

251
        // Previously a sweep tx was serialized at this point. Refactoring
252
        // removed this, but keep in mind that this data may still be present in
253
        // the database.
254

255
        return nil
1✔
256
}
257

258
// newCommitSweepResolverFromReader attempts to decode an encoded
259
// ContractResolver from the passed Reader instance, returning an active
260
// ContractResolver instance.
261
func newCommitSweepResolverFromReader(r io.Reader, resCfg ResolverConfig) (
262
        *commitSweepResolver, error) {
1✔
263

1✔
264
        c := &commitSweepResolver{
1✔
265
                contractResolverKit: *newContractResolverKit(resCfg),
1✔
266
        }
1✔
267

1✔
268
        if err := decodeCommitResolution(r, &c.commitResolution); err != nil {
1✔
269
                return nil, err
×
270
        }
×
271

272
        var resolved bool
1✔
273
        if err := binary.Read(r, endian, &resolved); err != nil {
1✔
274
                return nil, err
×
275
        }
×
276
        if resolved {
1✔
277
                c.markResolved()
×
278
        }
×
279

280
        if err := binary.Read(r, endian, &c.confirmHeight); err != nil {
1✔
281
                return nil, err
×
282
        }
×
283
        _, err := io.ReadFull(r, c.chanPoint.Hash[:])
1✔
284
        if err != nil {
1✔
285
                return nil, err
×
286
        }
×
287
        err = binary.Read(r, endian, &c.chanPoint.Index)
1✔
288
        if err != nil {
1✔
289
                return nil, err
×
290
        }
×
291

292
        // Previously a sweep tx was deserialized at this point. Refactoring
293
        // removed this, but keep in mind that this data may still be present in
294
        // the database.
295

296
        c.initLogger(fmt.Sprintf("%T(%v)", c, c.commitResolution.SelfOutPoint))
1✔
297
        c.initReport()
1✔
298

1✔
299
        return c, nil
1✔
300
}
301

302
// report returns a report on the resolution state of the contract.
303
func (c *commitSweepResolver) report() *ContractReport {
6✔
304
        c.reportLock.Lock()
6✔
305
        defer c.reportLock.Unlock()
6✔
306

6✔
307
        cpy := c.currentReport
6✔
308
        return &cpy
6✔
309
}
6✔
310

311
// initReport initializes the pending channels report for this resolver.
312
func (c *commitSweepResolver) initReport() {
4✔
313
        amt := btcutil.Amount(
4✔
314
                c.commitResolution.SelfOutputSignDesc.Output.Value,
4✔
315
        )
4✔
316

4✔
317
        // Set the initial report. All fields are filled in, except for the
4✔
318
        // maturity height which remains 0 until Resolve() is executed.
4✔
319
        //
4✔
320
        // TODO(joostjager): Resolvers only activate after the commit tx
4✔
321
        // confirms. With more refactoring in channel arbitrator, it would be
4✔
322
        // possible to make the confirmation height part of ResolverConfig and
4✔
323
        // populate MaturityHeight here.
4✔
324
        c.currentReport = ContractReport{
4✔
325
                Outpoint:         c.commitResolution.SelfOutPoint,
4✔
326
                Type:             ReportOutputUnencumbered,
4✔
327
                Amount:           amt,
4✔
328
                LimboBalance:     amt,
4✔
329
                RecoveredBalance: 0,
4✔
330
        }
4✔
331
}
4✔
332

333
// A compile time assertion to ensure commitSweepResolver meets the
334
// ContractResolver interface.
335
var _ reportingContractResolver = (*commitSweepResolver)(nil)
336

337
// Launch constructs a commit input and offers it to the sweeper.
338
func (c *commitSweepResolver) Launch() error {
3✔
339
        if c.isLaunched() {
3✔
340
                c.log.Tracef("already launched")
×
341
                return nil
×
342
        }
×
343

344
        c.log.Debugf("launching resolver...")
3✔
345
        c.markLaunched()
3✔
346

3✔
347
        // If we're already resolved, then we can exit early.
3✔
348
        if c.IsResolved() {
3✔
349
                c.log.Errorf("already resolved")
×
350
                return nil
×
351
        }
×
352

353
        // Wait up until the CSV expires, unless we also have a CLTV that
354
        // expires after.
355
        unlockHeight := c.confirmHeight + c.commitResolution.MaturityDelay
3✔
356
        if c.hasCLTV() {
3✔
357
                unlockHeight = max(unlockHeight, c.leaseExpiry)
×
358
        }
×
359

360
        // Update report with the calculated maturity height.
361
        c.reportLock.Lock()
3✔
362
        c.currentReport.MaturityHeight = unlockHeight
3✔
363
        c.reportLock.Unlock()
3✔
364

3✔
365
        // Derive the witness type for this input.
3✔
366
        witnessType, err := c.decideWitnessType()
3✔
367
        if err != nil {
3✔
368
                return err
×
369
        }
×
370

371
        // We'll craft an input with all the information required for the
372
        // sweeper to create a fully valid sweeping transaction to recover
373
        // these coins.
374
        var inp *input.BaseInput
3✔
375
        if c.hasCLTV() {
3✔
376
                inp = input.NewCsvInputWithCltv(
×
377
                        &c.commitResolution.SelfOutPoint, witnessType,
×
378
                        &c.commitResolution.SelfOutputSignDesc,
×
379
                        c.confirmHeight, c.commitResolution.MaturityDelay,
×
380
                        c.leaseExpiry, input.WithResolutionBlob(
×
381
                                c.commitResolution.ResolutionBlob,
×
382
                        ),
×
383
                )
×
384
        } else {
3✔
385
                inp = input.NewCsvInput(
3✔
386
                        &c.commitResolution.SelfOutPoint, witnessType,
3✔
387
                        &c.commitResolution.SelfOutputSignDesc,
3✔
388
                        c.confirmHeight, c.commitResolution.MaturityDelay,
3✔
389
                        input.WithResolutionBlob(
3✔
390
                                c.commitResolution.ResolutionBlob,
3✔
391
                        ),
3✔
392
                )
3✔
393
        }
3✔
394

395
        // TODO(roasbeef): instead of adding ctrl block to the sign desc, make
396
        // new input type, have sweeper set it?
397

398
        // Calculate the budget for the sweeping this input.
399
        budget := calculateBudget(
3✔
400
                btcutil.Amount(inp.SignDesc().Output.Value),
3✔
401
                c.Budget.ToLocalRatio, c.Budget.ToLocal,
3✔
402
        )
3✔
403
        c.log.Infof("sweeping commit output %v using budget=%v", witnessType,
3✔
404
                budget)
3✔
405

3✔
406
        // With our input constructed, we'll now offer it to the sweeper.
3✔
407
        resultChan, err := c.Sweeper.SweepInput(
3✔
408
                inp, sweep.Params{
3✔
409
                        Budget: budget,
3✔
410

3✔
411
                        // Specify a nil deadline here as there's no time
3✔
412
                        // pressure.
3✔
413
                        DeadlineHeight: fn.None[int32](),
3✔
414
                },
3✔
415
        )
3✔
416
        if err != nil {
3✔
417
                c.log.Errorf("unable to sweep input: %v", err)
×
418

×
419
                return err
×
420
        }
×
421

422
        c.sweepResultChan = resultChan
3✔
423

3✔
424
        return nil
3✔
425
}
426

427
// decideWitnessType returns the witness type for the input.
428
func (c *commitSweepResolver) decideWitnessType() (input.WitnessType, error) {
3✔
429
        var (
3✔
430
                isLocalCommitTx bool
3✔
431
                signDesc        = c.commitResolution.SelfOutputSignDesc
3✔
432
        )
3✔
433

3✔
434
        switch {
3✔
435
        // For taproot channels, we'll know if this is the local commit based
436
        // on the timelock value. For remote commitment transactions, the
437
        // witness script has a timelock of 1.
438
        case c.chanType.IsTaproot():
×
439
                delayKey := c.localChanCfg.DelayBasePoint.PubKey
×
440
                nonDelayKey := c.localChanCfg.PaymentBasePoint.PubKey
×
441

×
442
                signKey := c.commitResolution.SelfOutputSignDesc.KeyDesc.PubKey
×
443

×
444
                // If the key in the script is neither of these, we shouldn't
×
445
                // proceed. This should be impossible.
×
446
                if !signKey.IsEqual(delayKey) && !signKey.IsEqual(nonDelayKey) {
×
447
                        return nil, fmt.Errorf("unknown sign key %v", signKey)
×
448
                }
×
449

450
                // The commitment transaction is ours iff the signing key is
451
                // the delay key.
452
                isLocalCommitTx = signKey.IsEqual(delayKey)
×
453

454
        // The output is on our local commitment if the script starts with
455
        // OP_IF for the revocation clause. On the remote commitment it will
456
        // either be a regular P2WKH or a simple sig spend with a CSV delay.
457
        default:
3✔
458
                isLocalCommitTx = signDesc.WitnessScript[0] == txscript.OP_IF
3✔
459
        }
460

461
        isDelayedOutput := c.commitResolution.MaturityDelay != 0
3✔
462

3✔
463
        c.log.Debugf("isDelayedOutput=%v, isLocalCommitTx=%v", isDelayedOutput,
3✔
464
                isLocalCommitTx)
3✔
465

3✔
466
        // There're three types of commitments, those that have tweaks for the
3✔
467
        // remote key (us in this case), those that don't, and a third where
3✔
468
        // there is no tweak and the output is delayed. On the local commitment
3✔
469
        // our output will always be delayed. We'll rely on the presence of the
3✔
470
        // commitment tweak to discern which type of commitment this is.
3✔
471
        var witnessType input.WitnessType
3✔
472
        switch {
3✔
473
        // The local delayed output for a taproot channel.
474
        case isLocalCommitTx && c.chanType.IsTaproot():
×
475
                witnessType = input.TaprootLocalCommitSpend
×
476

477
        // The CSV 1 delayed output for a taproot channel.
478
        case !isLocalCommitTx && c.chanType.IsTaproot():
×
479
                witnessType = input.TaprootRemoteCommitSpend
×
480

481
        // Delayed output to us on our local commitment for a channel lease in
482
        // which we are the initiator.
483
        case isLocalCommitTx && c.hasCLTV():
×
484
                witnessType = input.LeaseCommitmentTimeLock
×
485

486
        // Delayed output to us on our local commitment.
487
        case isLocalCommitTx:
×
488
                witnessType = input.CommitmentTimeLock
×
489

490
        // A confirmed output to us on the remote commitment for a channel lease
491
        // in which we are the initiator.
492
        case isDelayedOutput && c.hasCLTV():
×
493
                witnessType = input.LeaseCommitmentToRemoteConfirmed
×
494

495
        // A confirmed output to us on the remote commitment.
496
        case isDelayedOutput:
2✔
497
                witnessType = input.CommitmentToRemoteConfirmed
2✔
498

499
        // A non-delayed output on the remote commitment where the key is
500
        // tweakless.
501
        case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil:
1✔
502
                witnessType = input.CommitSpendNoDelayTweakless
1✔
503

504
        // A non-delayed output on the remote commitment where the key is
505
        // tweaked.
506
        default:
×
507
                witnessType = input.CommitmentNoDelay
×
508
        }
509

510
        return witnessType, nil
3✔
511
}
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