-
Notifications
You must be signed in to change notification settings - Fork 2k
/
htlc_timeout_resolver.go
1083 lines (930 loc) · 35.2 KB
/
htlc_timeout_resolver.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package contractcourt
import (
"encoding/binary"
"fmt"
"io"
"sync"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/sweep"
)
// htlcTimeoutResolver is a ContractResolver that's capable of resolving an
// outgoing HTLC. The HTLC may be on our commitment transaction, or on the
// commitment transaction of the remote party. An output on our commitment
// transaction is considered fully resolved once the second-level transaction
// has been confirmed (and reached a sufficient depth). An output on the
// commitment transaction of the remote party is resolved once we detect a
// spend of the direct HTLC output using the timeout clause.
type htlcTimeoutResolver struct {
// htlcResolution contains all the information required to properly
// resolve this outgoing HTLC.
htlcResolution lnwallet.OutgoingHtlcResolution
// outputIncubating returns true if we've sent the output to the output
// incubator (utxo nursery).
outputIncubating bool
// resolved reflects if the contract has been fully resolved or not.
resolved bool
// broadcastHeight is the height that the original contract was
// broadcast to the main-chain at. We'll use this value to bound any
// historical queries to the chain for spends/confirmations.
//
// TODO(roasbeef): wrap above into definite resolution embedding?
broadcastHeight uint32
// htlc contains information on the htlc that we are resolving on-chain.
htlc channeldb.HTLC
// currentReport stores the current state of the resolver for reporting
// over the rpc interface. This should only be reported in case we have
// a non-nil SignDetails on the htlcResolution, otherwise the nursery
// will produce reports.
currentReport ContractReport
// reportLock prevents concurrent access to the resolver report.
reportLock sync.Mutex
contractResolverKit
htlcLeaseResolver
}
// newTimeoutResolver instantiates a new timeout htlc resolver.
func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution,
broadcastHeight uint32, htlc channeldb.HTLC,
resCfg ResolverConfig) *htlcTimeoutResolver {
h := &htlcTimeoutResolver{
contractResolverKit: *newContractResolverKit(resCfg),
htlcResolution: res,
broadcastHeight: broadcastHeight,
htlc: htlc,
}
h.initReport()
return h
}
// isTaproot returns true if the htlc output is a taproot output.
func (h *htlcTimeoutResolver) isTaproot() bool {
return txscript.IsPayToTaproot(
h.htlcResolution.SweepSignDesc.Output.PkScript,
)
}
// ResolverKey returns an identifier which should be globally unique for this
// particular resolver within the chain the original contract resides within.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcTimeoutResolver) ResolverKey() []byte {
// The primary key for this resolver will be the outpoint of the HTLC
// on the commitment transaction itself. If this is our commitment,
// then the output can be found within the signed timeout tx,
// otherwise, it's just the ClaimOutpoint.
var op wire.OutPoint
if h.htlcResolution.SignedTimeoutTx != nil {
op = h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint
} else {
op = h.htlcResolution.ClaimOutpoint
}
key := newResolverID(op)
return key[:]
}
const (
// expectedRemoteWitnessSuccessSize is the expected size of the witness
// on the remote commitment transaction for an outgoing HTLC that is
// swept on-chain by them with pre-image.
expectedRemoteWitnessSuccessSize = 5
// remotePreimageIndex index within the witness on the remote
// commitment transaction that will hold they pre-image if they go to
// sweep it on chain.
remotePreimageIndex = 3
// localPreimageIndex is the index within the witness on the local
// commitment transaction for an outgoing HTLC that will hold the
// pre-image if the remote party sweeps it.
localPreimageIndex = 1
// remoteTaprootWitnessSuccessSize is the expected size of the witness
// on the remote commitment for taproot channels. The spend path will
// look like
// - <sender sig> <receiver sig> <preimage> <success_script>
// <control_block>
remoteTaprootWitnessSuccessSize = 5
// taprootRemotePreimageIndex is the index within the witness on the
// taproot remote commitment spend that'll hold the pre-image if the
// remote party sweeps it.
taprootRemotePreimageIndex = 2
)
// claimCleanUp is a helper method that's called once the HTLC output is spent
// by the remote party. It'll extract the preimage, add it to the global cache,
// and finally send the appropriate clean up message.
func (h *htlcTimeoutResolver) claimCleanUp(
commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) {
// Depending on if this is our commitment or not, then we'll be looking
// for a different witness pattern.
spenderIndex := commitSpend.SpenderInputIndex
spendingInput := commitSpend.SpendingTx.TxIn[spenderIndex]
log.Infof("%T(%v): extracting preimage! remote party spent "+
"HTLC with tx=%v", h, h.htlcResolution.ClaimOutpoint,
spew.Sdump(commitSpend.SpendingTx))
// If this is the remote party's commitment, then we'll be looking for
// them to spend using the second-level success transaction.
var preimageBytes []byte
switch {
// For taproot channels, if the remote party has swept the HTLC, then
// the witness stack will look like:
//
// - <sender sig> <receiver sig> <preimage> <success_script>
// <control_block>
case h.isTaproot() && h.htlcResolution.SignedTimeoutTx == nil:
//nolint:lll
preimageBytes = spendingInput.Witness[taprootRemotePreimageIndex]
// The witness stack when the remote party sweeps the output on a
// regular channel to them looks like:
//
// - <0> <sender sig> <recvr sig> <preimage> <witness script>
case !h.isTaproot() && h.htlcResolution.SignedTimeoutTx == nil:
preimageBytes = spendingInput.Witness[remotePreimageIndex]
// If this is a taproot channel, and there's only a single witness
// element, then we're actually on the losing side of a breach
// attempt...
case h.isTaproot() && len(spendingInput.Witness) == 1:
return nil, fmt.Errorf("breach attempt failed")
// Otherwise, they'll be spending directly from our commitment output.
// In which case the witness stack looks like:
//
// - <sig> <preimage> <witness script>
//
// For taproot channels, this looks like:
// - <receiver sig> <preimage> <success_script> <control_block>
//
// So we can target the same index.
default:
preimageBytes = spendingInput.Witness[localPreimageIndex]
}
preimage, err := lntypes.MakePreimage(preimageBytes)
if err != nil {
return nil, fmt.Errorf("unable to create pre-image from "+
"witness: %v", err)
}
log.Infof("%T(%v): extracting preimage=%v from on-chain "+
"spend!", h, h.htlcResolution.ClaimOutpoint, preimage)
// With the preimage obtained, we can now add it to the global cache.
if err := h.PreimageDB.AddPreimages(preimage); err != nil {
log.Errorf("%T(%v): unable to add witness to cache",
h, h.htlcResolution.ClaimOutpoint)
}
var pre [32]byte
copy(pre[:], preimage[:])
// Finally, we'll send the clean up message, mark ourselves as
// resolved, then exit.
if err := h.DeliverResolutionMsg(ResolutionMsg{
SourceChan: h.ShortChanID,
HtlcIndex: h.htlc.HtlcIndex,
PreImage: &pre,
}); err != nil {
return nil, err
}
h.resolved = true
// Checkpoint our resolver with a report which reflects the preimage
// claim by the remote party.
amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value)
report := &channeldb.ResolverReport{
OutPoint: h.htlcResolution.ClaimOutpoint,
Amount: amt,
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
SpendTxID: commitSpend.SpenderTxHash,
}
return nil, h.Checkpoint(h, report)
}
// chainDetailsToWatch returns the output and script which we use to watch for
// spends from the direct HTLC output on the commitment transaction.
func (h *htlcTimeoutResolver) chainDetailsToWatch() (*wire.OutPoint, []byte, error) {
// If there's no timeout transaction, it means we are spending from a
// remote commit, then the claim output is the output directly on the
// commitment transaction, so we'll just use that.
if h.htlcResolution.SignedTimeoutTx == nil {
outPointToWatch := h.htlcResolution.ClaimOutpoint
scriptToWatch := h.htlcResolution.SweepSignDesc.Output.PkScript
return &outPointToWatch, scriptToWatch, nil
}
// If SignedTimeoutTx is not nil, this is the local party's commitment,
// and we'll need to grab watch the output that our timeout transaction
// points to. We can directly grab the outpoint, then also extract the
// witness script (the last element of the witness stack) to
// re-construct the pkScript we need to watch.
//
//nolint:lll
outPointToWatch := h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint
witness := h.htlcResolution.SignedTimeoutTx.TxIn[0].Witness
var (
scriptToWatch []byte
err error
)
switch {
// For taproot channels, then final witness element is the control
// block, and the one before it the witness script. We can use both of
// these together to reconstruct the taproot output key, then map that
// into a v1 witness program.
case h.isTaproot():
// First, we'll parse the control block into something we can
// use.
ctrlBlockBytes := witness[len(witness)-1]
ctrlBlock, err := txscript.ParseControlBlock(ctrlBlockBytes)
if err != nil {
return nil, nil, err
}
// With the control block, we'll grab the witness script, then
// use that to derive the tapscript root.
witnessScript := witness[len(witness)-2]
tapscriptRoot := ctrlBlock.RootHash(witnessScript)
// Once we have the root, then we can derive the output key
// from the internal key, then turn that into a witness
// program.
outputKey := txscript.ComputeTaprootOutputKey(
ctrlBlock.InternalKey, tapscriptRoot,
)
scriptToWatch, err = txscript.PayToTaprootScript(outputKey)
if err != nil {
return nil, nil, err
}
// For regular channels, the witness script is the last element on the
// stack. We can then use this to re-derive the output that we're
// watching on chain.
default:
scriptToWatch, err = input.WitnessScriptHash(
witness[len(witness)-1],
)
}
if err != nil {
return nil, nil, err
}
return &outPointToWatch, scriptToWatch, nil
}
// isPreimageSpend returns true if the passed spend on the specified commitment
// is a success spend that reveals the pre-image or not.
func isPreimageSpend(isTaproot bool, spend *chainntnfs.SpendDetail,
localCommit bool) bool {
// Based on the spending input index and transaction, obtain the
// witness that tells us what type of spend this is.
spenderIndex := spend.SpenderInputIndex
spendingInput := spend.SpendingTx.TxIn[spenderIndex]
spendingWitness := spendingInput.Witness
switch {
// If this is a taproot remote commitment, then we can detect the type
// of spend via the leaf revealed in the control block and the witness
// itself.
//
// The keyspend (revocation path) is just a single signature, while the
// timeout and success paths are most distinct.
//
// The success path will look like:
//
// - <sender sig> <receiver sig> <preimage> <success_script>
// <control_block>
case isTaproot && !localCommit:
preImageIdx := taprootRemotePreimageIndex
//nolint:lll
return len(spendingWitness) == remoteTaprootWitnessSuccessSize &&
len(spendingWitness[preImageIdx]) == lntypes.HashSize
// Otherwise, then if this is our local commitment transaction, then if
// they're sweeping the transaction, it'll be directly from the output,
// skipping the second level.
//
// In this case, then there're two main tapscript paths, with the
// success case look like:
//
// - <receiver sig> <preimage> <success_script> <control_block>
case isTaproot && localCommit:
return len(spendingWitness[localPreimageIndex]) ==
lntypes.HashSize
// If this is the non-taproot, remote commitment then the only possible
// spends for outgoing HTLCs are:
//
// RECVR: <0> <sender sig> <recvr sig> <preimage> (2nd level success spend)
// REVOK: <sig> <key>
// SENDR: <sig> 0
//
// In this case, if 5 witness elements are present (factoring the
// witness script), and the 3rd element is the size of the pre-image,
// then this is a remote spend. If not, then we swept it ourselves, or
// revoked their output.
case !isTaproot && !localCommit:
return len(spendingWitness) == expectedRemoteWitnessSuccessSize &&
len(spendingWitness[remotePreimageIndex]) ==
lntypes.HashSize
// Otherwise, for our non-taproot commitment, the only possible spends
// for an outgoing HTLC are:
//
// SENDR: <0> <sendr sig> <recvr sig> <0> (2nd level timeout)
// RECVR: <recvr sig> <preimage>
// REVOK: <revoke sig> <revoke key>
//
// So the only success case has the pre-image as the 2nd (index 1)
// element in the witness.
case !isTaproot:
fallthrough
default:
return len(spendingWitness[localPreimageIndex]) ==
lntypes.HashSize
}
}
// Resolve kicks off full resolution of an outgoing HTLC output. If it's our
// commitment, it isn't resolved until we see the second level HTLC txn
// confirmed. If it's the remote party's commitment, we don't resolve until we
// see a direct sweep via the timeout clause.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
// If we're already resolved, then we can exit early.
if h.resolved {
return nil, nil
}
// Start by spending the HTLC output, either by broadcasting the
// second-level timeout transaction, or directly if this is the remote
// commitment.
commitSpend, err := h.spendHtlcOutput()
if err != nil {
return nil, err
}
// If the spend reveals the pre-image, then we'll enter the clean up
// workflow to pass the pre-image back to the incoming link, add it to
// the witness cache, and exit.
if isPreimageSpend(
h.isTaproot(), commitSpend,
h.htlcResolution.SignedTimeoutTx != nil,
) {
log.Infof("%T(%v): HTLC has been swept with pre-image by "+
"remote party during timeout flow! Adding pre-image to "+
"witness cache", h.htlcResolution.ClaimOutpoint)
return h.claimCleanUp(commitSpend)
}
log.Infof("%T(%v): resolving htlc with incoming fail msg, fully "+
"confirmed", h, h.htlcResolution.ClaimOutpoint)
// At this point, the second-level transaction is sufficiently
// confirmed, or a transaction directly spending the output is.
// Therefore, we can now send back our clean up message, failing the
// HTLC on the incoming link.
failureMsg := &lnwire.FailPermanentChannelFailure{}
if err := h.DeliverResolutionMsg(ResolutionMsg{
SourceChan: h.ShortChanID,
HtlcIndex: h.htlc.HtlcIndex,
Failure: failureMsg,
}); err != nil {
return nil, err
}
// Depending on whether this was a local or remote commit, we must
// handle the spending transaction accordingly.
return h.handleCommitSpend(commitSpend)
}
// sweepSecondLevelTx sends a second level timeout transaction to the sweeper.
// This transaction uses the SINLGE|ANYONECANPAY flag.
func (h *htlcTimeoutResolver) sweepSecondLevelTx() error {
log.Infof("%T(%x): offering second-layer timeout tx to sweeper: %v",
h, h.htlc.RHash[:],
spew.Sdump(h.htlcResolution.SignedTimeoutTx))
var inp input.Input
if h.isTaproot() {
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutTaprootInput(
h.htlcResolution.SignedTimeoutTx,
h.htlcResolution.SignDetails,
h.broadcastHeight,
))
} else {
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutAnchorInput(
h.htlcResolution.SignedTimeoutTx,
h.htlcResolution.SignDetails,
h.broadcastHeight,
))
}
_, err := h.Sweeper.SweepInput(
inp,
sweep.Params{
Fee: sweep.FeePreference{
ConfTarget: secondLevelConfTarget,
},
},
)
if err != nil {
return err
}
// TODO(yy): checkpoint here?
return err
}
// sendSecondLevelTxLegacy sends a second level timeout transaction to the utxo
// nursery. This transaction uses the legacy SIGHASH_ALL flag.
func (h *htlcTimeoutResolver) sendSecondLevelTxLegacy() error {
log.Debugf("%T(%v): incubating htlc output", h,
h.htlcResolution.ClaimOutpoint)
err := h.IncubateOutputs(
h.ChanPoint, &h.htlcResolution, nil,
h.broadcastHeight,
)
if err != nil {
return err
}
h.outputIncubating = true
return h.Checkpoint(h)
}
// spendHtlcOutput handles the initial spend of an HTLC output via the timeout
// clause. If this is our local commitment, the second-level timeout TX will be
// used to spend the output into the next stage. If this is the remote
// commitment, the output will be swept directly without the timeout
// transaction.
func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) {
switch {
// If we have non-nil SignDetails, this means that have a 2nd level
// HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY
// (the case for anchor type channels). In this case we can re-sign it
// and attach fees at will. We let the sweeper handle this job.
case h.htlcResolution.SignDetails != nil && !h.outputIncubating:
if err := h.sweepSecondLevelTx(); err != nil {
log.Errorf("Sending timeout tx to sweeper: %v", err)
return nil, err
}
// If we have no SignDetails, and we haven't already sent the output to
// the utxo nursery, then we'll do so now.
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
if err := h.sendSecondLevelTxLegacy(); err != nil {
log.Errorf("Sending timeout tx to nursery: %v", err)
return nil, err
}
}
// Now that we've handed off the HTLC to the nursery or sweeper, we'll
// watch for a spend of the output, and make our next move off of that.
// Depending on if this is our commitment, or the remote party's
// commitment, we'll be watching a different outpoint and script.
return h.watchHtlcSpend()
}
// watchHtlcSpend watches for a spend of the HTLC output. For neutrino backend,
// it will check blocks for the confirmed spend. For btcd and bitcoind, it will
// check both the mempool and the blocks.
func (h *htlcTimeoutResolver) watchHtlcSpend() (*chainntnfs.SpendDetail,
error) {
// TODO(yy): outpointToWatch is always h.HtlcOutpoint(), can refactor
// to remove the redundancy.
outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
if err != nil {
return nil, err
}
// If there's no mempool configured, which is the case for SPV node
// such as neutrino, then we will watch for confirmed spend only.
if h.Mempool == nil {
return h.waitForConfirmedSpend(outpointToWatch, scriptToWatch)
}
// Watch for a spend of the HTLC output in both the mempool and blocks.
return h.waitForMempoolOrBlockSpend(*outpointToWatch, scriptToWatch)
}
// waitForConfirmedSpend waits for the HTLC output to be spent and confirmed in
// a block, returns the spend details.
func (h *htlcTimeoutResolver) waitForConfirmedSpend(op *wire.OutPoint,
pkScript []byte) (*chainntnfs.SpendDetail, error) {
log.Infof("%T(%v): waiting for spent of HTLC output %v to be "+
"fully confirmed", h, h.htlcResolution.ClaimOutpoint, op)
// We'll block here until either we exit, or the HTLC output on the
// commitment transaction has been spent.
spend, err := waitForSpend(
op, pkScript, h.broadcastHeight, h.Notifier, h.quit,
)
if err != nil {
return nil, err
}
// Once confirmed, persist the state on disk.
if err := h.checkPointSecondLevelTx(); err != nil {
return nil, err
}
return spend, err
}
// checkPointSecondLevelTx persists the state of a second level HTLC tx to disk
// if it's published by the sweeper.
func (h *htlcTimeoutResolver) checkPointSecondLevelTx() error {
// If this was the second level transaction published by the sweeper,
// we can checkpoint the resolver now that it's confirmed.
if h.htlcResolution.SignDetails != nil && !h.outputIncubating {
h.outputIncubating = true
if err := h.Checkpoint(h); err != nil {
log.Errorf("unable to Checkpoint: %v", err)
return err
}
}
return nil
}
// handleCommitSpend handles the spend of the HTLC output on the commitment
// transaction. If this was our local commitment, the spend will be he
// confirmed second-level timeout transaction, and we'll sweep that into our
// wallet. If the was a remote commitment, the resolver will resolve
// immetiately.
func (h *htlcTimeoutResolver) handleCommitSpend(
commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) {
var (
// claimOutpoint will be the outpoint of the second level
// transaction, or on the remote commitment directly. It will
// start out as set in the resolution, but we'll update it if
// the second-level goes through the sweeper and changes its
// txid.
claimOutpoint = h.htlcResolution.ClaimOutpoint
// spendTxID will be the ultimate spend of the claimOutpoint.
// We set it to the commit spend for now, as this is the
// ultimate spend in case this is a remote commitment. If we go
// through the second-level transaction, we'll update this
// accordingly.
spendTxID = commitSpend.SpenderTxHash
reports []*channeldb.ResolverReport
)
switch {
// If the sweeper is handling the second level transaction, wait for
// the CSV and possible CLTV lock to expire, before sweeping the output
// on the second-level.
case h.htlcResolution.SignDetails != nil:
waitHeight := h.deriveWaitHeight(
h.htlcResolution.CsvDelay, commitSpend,
)
h.reportLock.Lock()
h.currentReport.Stage = 2
h.currentReport.MaturityHeight = waitHeight
h.reportLock.Unlock()
if h.hasCLTV() {
log.Infof("%T(%x): waiting for CSV and CLTV lock to "+
"expire at height %v", h, h.htlc.RHash[:],
waitHeight)
} else {
log.Infof("%T(%x): waiting for CSV lock to expire at "+
"height %v", h, h.htlc.RHash[:], waitHeight)
}
err := waitForHeight(waitHeight, h.Notifier, h.quit)
if err != nil {
return nil, err
}
// We'll use this input index to determine the second-level
// output index on the transaction, as the signatures requires
// the indexes to be the same. We don't look for the
// second-level output script directly, as there might be more
// than one HTLC output to the same pkScript.
op := &wire.OutPoint{
Hash: *commitSpend.SpenderTxHash,
Index: commitSpend.SpenderInputIndex,
}
var csvWitnessType input.StandardWitnessType
if h.isTaproot() {
//nolint:lll
csvWitnessType = input.TaprootHtlcOfferedTimeoutSecondLevel
} else {
csvWitnessType = input.HtlcOfferedTimeoutSecondLevel
}
// Let the sweeper sweep the second-level output now that the
// CSV/CLTV locks have expired.
inp := h.makeSweepInput(
op, csvWitnessType,
input.LeaseHtlcOfferedTimeoutSecondLevel,
&h.htlcResolution.SweepSignDesc,
h.htlcResolution.CsvDelay, h.broadcastHeight,
h.htlc.RHash,
)
_, err = h.Sweeper.SweepInput(
inp,
sweep.Params{
Fee: sweep.FeePreference{
ConfTarget: sweepConfTarget,
},
},
)
if err != nil {
return nil, err
}
// Update the claim outpoint to point to the second-level
// transaction created by the sweeper.
claimOutpoint = *op
fallthrough
// Finally, if this was an output on our commitment transaction, we'll
// wait for the second-level HTLC output to be spent, and for that
// transaction itself to confirm.
case h.htlcResolution.SignedTimeoutTx != nil:
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
"delayed output", h, claimOutpoint)
sweepTx, err := waitForSpend(
&claimOutpoint,
h.htlcResolution.SweepSignDesc.Output.PkScript,
h.broadcastHeight, h.Notifier, h.quit,
)
if err != nil {
return nil, err
}
// Update the spend txid to the hash of the sweep transaction.
spendTxID = sweepTx.SpenderTxHash
// Once our sweep of the timeout tx has confirmed, we add a
// resolution for our timeoutTx tx first stage transaction.
timeoutTx := commitSpend.SpendingTx
index := commitSpend.SpenderInputIndex
spendHash := commitSpend.SpenderTxHash
reports = append(reports, &channeldb.ResolverReport{
OutPoint: timeoutTx.TxIn[index].PreviousOutPoint,
Amount: h.htlc.Amt.ToSatoshis(),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
SpendTxID: spendHash,
})
}
// With the clean up message sent, we'll now mark the contract
// resolved, update the recovered balance, record the timeout and the
// sweep txid on disk, and wait.
h.resolved = true
h.reportLock.Lock()
h.currentReport.RecoveredBalance = h.currentReport.LimboBalance
h.currentReport.LimboBalance = 0
h.reportLock.Unlock()
amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value)
reports = append(reports, &channeldb.ResolverReport{
OutPoint: claimOutpoint,
Amount: amt,
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
SpendTxID: spendTxID,
})
return nil, h.Checkpoint(h, reports...)
}
// Stop signals the resolver to cancel any current resolution processes, and
// suspend.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcTimeoutResolver) Stop() {
close(h.quit)
}
// IsResolved returns true if the stored state in the resolve is fully
// resolved. In this case the target output can be forgotten.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcTimeoutResolver) IsResolved() bool {
return h.resolved
}
// report returns a report on the resolution state of the contract.
func (h *htlcTimeoutResolver) report() *ContractReport {
// If the sign details are nil, the report will be created by handled
// by the nursery.
if h.htlcResolution.SignDetails == nil {
return nil
}
h.reportLock.Lock()
defer h.reportLock.Unlock()
cpy := h.currentReport
return &cpy
}
func (h *htlcTimeoutResolver) initReport() {
// We create the initial report. This will only be reported for
// resolvers not handled by the nursery.
finalAmt := h.htlc.Amt.ToSatoshis()
if h.htlcResolution.SignedTimeoutTx != nil {
finalAmt = btcutil.Amount(
h.htlcResolution.SignedTimeoutTx.TxOut[0].Value,
)
}
h.currentReport = ContractReport{
Outpoint: h.htlcResolution.ClaimOutpoint,
Type: ReportOutputOutgoingHtlc,
Amount: finalAmt,
MaturityHeight: h.htlcResolution.Expiry,
LimboBalance: finalAmt,
Stage: 1,
}
}
// Encode writes an encoded version of the ContractResolver into the passed
// Writer.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcTimeoutResolver) Encode(w io.Writer) error {
// First, we'll write out the relevant fields of the
// OutgoingHtlcResolution to the writer.
if err := encodeOutgoingResolution(w, &h.htlcResolution); err != nil {
return err
}
// With that portion written, we can now write out the fields specific
// to the resolver itself.
if err := binary.Write(w, endian, h.outputIncubating); err != nil {
return err
}
if err := binary.Write(w, endian, h.resolved); err != nil {
return err
}
if err := binary.Write(w, endian, h.broadcastHeight); err != nil {
return err
}
if err := binary.Write(w, endian, h.htlc.HtlcIndex); err != nil {
return err
}
// We encode the sign details last for backwards compatibility.
err := encodeSignDetails(w, h.htlcResolution.SignDetails)
if err != nil {
return err
}
return nil
}
// newTimeoutResolverFromReader attempts to decode an encoded ContractResolver
// from the passed Reader instance, returning an active ContractResolver
// instance.
func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) (
*htlcTimeoutResolver, error) {
h := &htlcTimeoutResolver{
contractResolverKit: *newContractResolverKit(resCfg),
}
// First, we'll read out all the mandatory fields of the
// OutgoingHtlcResolution that we store.
if err := decodeOutgoingResolution(r, &h.htlcResolution); err != nil {
return nil, err
}
// With those fields read, we can now read back the fields that are
// specific to the resolver itself.
if err := binary.Read(r, endian, &h.outputIncubating); err != nil {
return nil, err
}
if err := binary.Read(r, endian, &h.resolved); err != nil {
return nil, err
}
if err := binary.Read(r, endian, &h.broadcastHeight); err != nil {
return nil, err
}
if err := binary.Read(r, endian, &h.htlc.HtlcIndex); err != nil {
return nil, err
}
// Sign details is a new field that was added to the htlc resolution,
// so it is serialized last for backwards compatibility. We try to read
// it, but don't error out if there are not bytes left.
signDetails, err := decodeSignDetails(r)
if err == nil {
h.htlcResolution.SignDetails = signDetails
} else if err != io.EOF && err != io.ErrUnexpectedEOF {
return nil, err
}
h.initReport()
return h, nil
}
// Supplement adds additional information to the resolver that is required
// before Resolve() is called.
//
// NOTE: Part of the htlcContractResolver interface.
func (h *htlcTimeoutResolver) Supplement(htlc channeldb.HTLC) {
h.htlc = htlc
}
// HtlcPoint returns the htlc's outpoint on the commitment tx.
//
// NOTE: Part of the htlcContractResolver interface.
func (h *htlcTimeoutResolver) HtlcPoint() wire.OutPoint {
return h.htlcResolution.HtlcPoint()
}
// A compile time assertion to ensure htlcTimeoutResolver meets the
// ContractResolver interface.
var _ htlcContractResolver = (*htlcTimeoutResolver)(nil)
// spendResult is used to hold the result of a spend event from either a
// mempool spend or a block spend.
type spendResult struct {
// spend contains the details of the spend.
spend *chainntnfs.SpendDetail
// err is the error that occurred during the spend notification.
err error
}
// waitForMempoolOrBlockSpend waits for the htlc output to be spent by a
// transaction that's either be found in the mempool or in a block.
func (h *htlcTimeoutResolver) waitForMempoolOrBlockSpend(op wire.OutPoint,
pkScript []byte) (*chainntnfs.SpendDetail, error) {
log.Infof("%T(%v): waiting for spent of HTLC output %v to be found "+
"in mempool or block", h, h.htlcResolution.ClaimOutpoint, op)
// Subscribe for block spent(confirmed).
blockSpent, err := h.Notifier.RegisterSpendNtfn(
&op, pkScript, h.broadcastHeight,
)
if err != nil {
return nil, fmt.Errorf("register spend: %w", err)
}
// Subscribe for mempool spent(unconfirmed).
mempoolSpent, err := h.Mempool.SubscribeMempoolSpent(op)
if err != nil {
return nil, fmt.Errorf("register mempool spend: %w", err)
}
// Create a result chan that will be used to receive the spending
// events.
result := make(chan *spendResult, 2)
// Create a goroutine that will wait for either a mempool spend or a
// block spend.
//
// NOTE: no need to use waitgroup here as when the resolver exits, the
// goroutine will return on the quit channel.
go h.consumeSpendEvents(result, blockSpent.Spend, mempoolSpent.Spend)
// Wait for the spend event to be received.
select {
case event := <-result:
// Cancel the mempool subscription as we don't need it anymore.
h.Mempool.CancelMempoolSpendEvent(mempoolSpent)
return event.spend, event.err
case <-h.quit:
return nil, errResolverShuttingDown
}
}
// consumeSpendEvents consumes the spend events from the block and mempool
// subscriptions. It exits when a spend event is received from the block, or
// the resolver itself quits. When a spend event is received from the mempool,
// however, it won't exit but continuing to wait for a spend event from the
// block subscription.
//
// NOTE: there could be a case where we found the preimage in the mempool,
// which will be added to our preimage beacon and settle the incoming link,
// meanwhile the timeout sweep tx confirms. This outgoing HTLC is "free" money
// and is not swept here.
//
// TODO(yy): sweep the outgoing htlc if it's confirmed.
func (h *htlcTimeoutResolver) consumeSpendEvents(resultChan chan *spendResult,
blockSpent, mempoolSpent <-chan *chainntnfs.SpendDetail) {
op := h.HtlcPoint()
// Create a result chan to hold the results.
result := &spendResult{}
// hasMempoolSpend is a flag that indicates whether we have found a
// preimage spend from the mempool. This is used to determine whether
// to checkpoint the resolver or not when later we found the
// corresponding block spend.
hasMempoolSpent := false
// Wait for a spend event to arrive.
for {
select {
// If a spend event is received from the block, this outgoing
// htlc is spent either by the remote via the preimage or by us
// via the timeout. We can exit the loop and `claimCleanUp`
// will feed the preimage to the beacon if found. This treats
// the block as the final judge and the preimage spent won't
// appear in the mempool afterwards.
//
// NOTE: if a reorg happens, the preimage spend can appear in
// the mempool again. Though a rare case, we should handle it
// in a dedicated reorg system.
case spendDetail, ok := <-blockSpent:
if !ok {
result.err = fmt.Errorf("block spent err: %w",
errResolverShuttingDown)
} else {
log.Debugf("Found confirmed spend of HTLC "+
"output %s in tx=%s", op,
spendDetail.SpenderTxHash)