forked from lightningnetwork/lnd
-
Notifications
You must be signed in to change notification settings - Fork 2
/
htlc_timeout_resolver.go
1317 lines (1121 loc) · 42.4 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/chaincfg/chainhash"
"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/fn/v2"
"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
// 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
// incomingHTLCExpiryHeight is the absolute block height at which the
// incoming HTLC will expire. This is used as the deadline height as
// the outgoing HTLC must be swept before its incoming HTLC expires.
incomingHTLCExpiryHeight fn.Option[int32]
}
// 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()
h.initLogger(fmt.Sprintf("%T(%v)", h, h.outpoint()))
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,
)
}
// outpoint returns the outpoint of the HTLC output we're attempting to sweep.
func (h *htlcTimeoutResolver) outpoint() wire.OutPoint {
// 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.
if h.htlcResolution.SignedTimeoutTx != nil {
return h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint
}
return h.htlcResolution.ClaimOutpoint
}
// 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 {
key := newResolverID(h.outpoint())
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
// expectedLocalWitnessSuccessSize is the expected size of the witness
// on the local commitment transaction for an outgoing HTLC that is
// swept on-chain by them with pre-image.
expectedLocalWitnessSuccessSize = 3
// 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
// localTaprootWitnessSuccessSize is the expected size of the witness
// on the local commitment for taproot channels. The spend path will
// look like
// - <receiver sig> <preimage> <success_script> <control_block>
localTaprootWitnessSuccessSize = 4
// 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) 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:ll
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 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 fmt.Errorf("unable to create pre-image from witness: %w",
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 err
}
h.markResolved()
// 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 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:ll
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:
return checkSizeAndIndex(
spendingWitness, remoteTaprootWitnessSuccessSize,
taprootRemotePreimageIndex,
)
// 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 checkSizeAndIndex(
spendingWitness, localTaprootWitnessSuccessSize,
localPreimageIndex,
)
// 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 checkSizeAndIndex(
spendingWitness, expectedRemoteWitnessSuccessSize,
remotePreimageIndex,
)
// 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 checkSizeAndIndex(
spendingWitness, expectedLocalWitnessSuccessSize,
localPreimageIndex,
)
}
}
// checkSizeAndIndex checks that the witness is of the expected size and that
// the witness element at the specified index is of the expected size.
func checkSizeAndIndex(witness wire.TxWitness, size, index int) bool {
if len(witness) != size {
return false
}
return len(witness[index]) == 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.IsResolved() {
h.log.Errorf("already resolved")
return nil, nil
}
// If this is an output on the remote party's commitment transaction,
// use the direct-spend path to sweep the htlc.
if h.isRemoteCommitOutput() {
return nil, h.resolveRemoteCommitOutput()
}
// If this is a zero-fee HTLC, we now handle the spend from our
// commitment transaction.
if h.isZeroFeeOutput() {
return nil, h.resolveTimeoutTx()
}
// If this is an output on our own commitment using pre-anchor channel
// type, we will let the utxo nursery handle it.
return nil, h.resolveSecondLevelTxLegacy()
}
// sweepTimeoutTx sends a second level timeout transaction to the sweeper.
// This transaction uses the SINLGE|ANYONECANPAY flag.
func (h *htlcTimeoutResolver) sweepTimeoutTx() error {
var inp input.Input
if h.isTaproot() {
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutTaprootInput(
h.htlcResolution.SignedTimeoutTx,
h.htlcResolution.SignDetails,
h.broadcastHeight,
input.WithResolutionBlob(
h.htlcResolution.ResolutionBlob,
),
))
} else {
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutAnchorInput(
h.htlcResolution.SignedTimeoutTx,
h.htlcResolution.SignDetails,
h.broadcastHeight,
))
}
// Calculate the budget.
//
// TODO(yy): the budget is twice the output's value, which is needed as
// we don't force sweep the output now. To prevent cascading force
// closes, we use all its output value plus a wallet input as the
// budget. This is a temporary solution until we can optionally cancel
// the incoming HTLC, more details in,
// - https://github.com/lightningnetwork/lnd/issues/7969
budget := calculateBudget(
btcutil.Amount(inp.SignDesc().Output.Value), 2, 0,
)
h.log.Infof("offering 2nd-level HTLC timeout tx to sweeper "+
"with deadline=%v, budget=%v", h.incomingHTLCExpiryHeight,
budget)
// For an outgoing HTLC, it must be swept before the RefundTimeout of
// its incoming HTLC is reached.
_, err := h.Sweeper.SweepInput(
inp,
sweep.Params{
Budget: budget,
DeadlineHeight: h.incomingHTLCExpiryHeight,
},
)
if err != nil {
return err
}
return err
}
// resolveSecondLevelTxLegacy sends a second level timeout transaction to the
// utxo nursery. This transaction uses the legacy SIGHASH_ALL flag.
func (h *htlcTimeoutResolver) resolveSecondLevelTxLegacy() error {
h.log.Debug("incubating htlc output")
// The utxo nursery will take care of broadcasting the second-level
// timeout tx and sweeping its output once it confirms.
err := h.IncubateOutputs(
h.ChanPoint, fn.Some(h.htlcResolution),
fn.None[lnwallet.IncomingHtlcResolution](),
h.broadcastHeight, h.incomingHTLCExpiryHeight,
)
if err != nil {
return err
}
return h.resolveTimeoutTx()
}
// sweepDirectHtlcOutput sends the direct spend of the HTLC output to the
// sweeper. This is used when the remote party goes on chain, and we're able to
// sweep an HTLC we offered after a timeout. Only the CLTV encumbered outputs
// are resolved via this path.
func (h *htlcTimeoutResolver) sweepDirectHtlcOutput() error {
var htlcWitnessType input.StandardWitnessType
if h.isTaproot() {
htlcWitnessType = input.TaprootHtlcOfferedRemoteTimeout
} else {
htlcWitnessType = input.HtlcOfferedRemoteTimeout
}
sweepInput := input.NewCsvInputWithCltv(
&h.htlcResolution.ClaimOutpoint, htlcWitnessType,
&h.htlcResolution.SweepSignDesc, h.broadcastHeight,
h.htlcResolution.CsvDelay, h.htlcResolution.Expiry,
input.WithResolutionBlob(h.htlcResolution.ResolutionBlob),
)
// Calculate the budget.
//
// TODO(yy): the budget is twice the output's value, which is needed as
// we don't force sweep the output now. To prevent cascading force
// closes, we use all its output value plus a wallet input as the
// budget. This is a temporary solution until we can optionally cancel
// the incoming HTLC, more details in,
// - https://github.com/lightningnetwork/lnd/issues/7969
budget := calculateBudget(
btcutil.Amount(sweepInput.SignDesc().Output.Value), 2, 0,
)
log.Infof("%T(%x): offering offered remote timeout HTLC output to "+
"sweeper with deadline %v and budget=%v at height=%v",
h, h.htlc.RHash[:], h.incomingHTLCExpiryHeight, budget,
h.broadcastHeight)
_, err := h.Sweeper.SweepInput(
sweepInput,
sweep.Params{
Budget: budget,
// This is an outgoing HTLC, so we want to make sure
// that we sweep it before the incoming HTLC expires.
DeadlineHeight: h.incomingHTLCExpiryHeight,
},
)
if err != nil {
return err
}
return nil
}
// 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) {
// 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
}
return spend, err
}
// Stop signals the resolver to cancel any current resolution processes, and
// suspend.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcTimeoutResolver) Stop() {
h.log.Debugf("stopping...")
defer h.log.Debugf("stopped")
close(h.quit)
}
// report returns a report on the resolution state of the contract.
func (h *htlcTimeoutResolver) report() *ContractReport {
// If we have a SignedTimeoutTx but no SignDetails, this is a local
// commitment for a non-anchor channel, which was handled by the utxo
// nursery.
if h.htlcResolution.SignDetails == nil && h.
htlcResolution.SignedTimeoutTx != 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,
)
}
// If there's no timeout transaction, then we're already effectively in
// level two.
stage := uint32(1)
if h.htlcResolution.SignedTimeoutTx == nil {
stage = 2
}
h.currentReport = ContractReport{
Outpoint: h.htlcResolution.ClaimOutpoint,
Type: ReportOutputOutgoingHtlc,
Amount: finalAmt,
MaturityHeight: h.htlcResolution.Expiry,
LimboBalance: finalAmt,
Stage: stage,
}
}
// 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.IsResolved()); 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
}
var resolved bool
if err := binary.Read(r, endian, &resolved); err != nil {
return nil, err
}
if resolved {
h.markResolved()
}
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()
h.initLogger(fmt.Sprintf("%T(%v)", h, h.outpoint()))
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()
}
// SupplementDeadline sets the incomingHTLCExpiryHeight for this outgoing htlc
// resolver.
//
// NOTE: Part of the htlcContractResolver interface.
func (h *htlcTimeoutResolver) SupplementDeadline(d fn.Option[int32]) {
h.incomingHTLCExpiryHeight = d
}
// 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{}
// 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)
result.spend = spendDetail
// Once confirmed, persist the state on disk if
// we haven't seen the output's spending tx in
// mempool before.
}
// Send the result and exit the loop.
resultChan <- result
return
// If a spend event is received from the mempool, this can be
// either the 2nd stage timeout tx or a preimage spend from the
// remote. We will further check whether the spend reveals the
// preimage and add it to the preimage beacon to settle the
// incoming link.
//
// NOTE: we won't exit the loop here so we can continue to
// watch for the block spend to check point the resolution.
case spendDetail, ok := <-mempoolSpent:
if !ok {
result.err = fmt.Errorf("mempool spent err: %w",
errResolverShuttingDown)
// This is an internal error so we exit.
resultChan <- result
return
}
log.Debugf("Found mempool spend of HTLC output %s "+
"in tx=%s", op, spendDetail.SpenderTxHash)
// Check whether the spend reveals the preimage, if not
// continue the loop.
hasPreimage := isPreimageSpend(
h.isTaproot(), spendDetail,
!h.isRemoteCommitOutput(),
)
if !hasPreimage {
log.Debugf("HTLC output %s spent doesn't "+
"reveal preimage", op)
continue
}
// Found the preimage spend, send the result and
// continue the loop.
result.spend = spendDetail
resultChan <- result
continue
// If the resolver exits, we exit the goroutine.
case <-h.quit:
result.err = errResolverShuttingDown
resultChan <- result
return
}
}
}
// isRemoteCommitOutput returns a bool to indicate whether the htlc output is
// on the remote commitment.
func (h *htlcTimeoutResolver) isRemoteCommitOutput() bool {
// If we don't have a timeout transaction, then this means that this is
// an output on the remote party's commitment transaction.
return h.htlcResolution.SignedTimeoutTx == nil
}
// isZeroFeeOutput returns a boolean indicating whether the htlc output is from
// a anchor-enabled channel, which uses the sighash SINGLE|ANYONECANPAY.
func (h *htlcTimeoutResolver) isZeroFeeOutput() bool {
// If we have non-nil SignDetails, this means it has 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.
return h.htlcResolution.SignedTimeoutTx != nil &&
h.htlcResolution.SignDetails != nil
}
// waitHtlcSpendAndCheckPreimage waits for the htlc output to be spent and
// checks whether the spending reveals the preimage. If the preimage is found,
// it will be added to the preimage beacon to settle the incoming link, and a
// nil spend details will be returned. Otherwise, the spend details will be
// returned, indicating this is a non-preimage spend.
func (h *htlcTimeoutResolver) waitHtlcSpendAndCheckPreimage() (
*chainntnfs.SpendDetail, error) {
// Wait for the htlc output to be spent, which can happen in one of the
// paths,
// 1. The remote party spends the htlc output using the preimage.
// 2. The local party spends the htlc timeout tx from the local
// commitment.
// 3. The local party spends the htlc output directlt from the remote
// commitment.
spend, err := h.watchHtlcSpend()
if err != nil {
return nil, err
}
// If the spend reveals the pre-image, then we'll enter the clean up
// workflow to pass the preimage back to the incoming link, add it to
// the witness cache, and exit.
if isPreimageSpend(h.isTaproot(), spend, !h.isRemoteCommitOutput()) {
return nil, h.claimCleanUp(spend)
}
return spend, nil
}
// sweepTimeoutTxOutput attempts to sweep the output of the second level
// timeout tx.
func (h *htlcTimeoutResolver) sweepTimeoutTxOutput() error {
h.log.Debugf("sweeping output %v from 2nd-level HTLC timeout tx",
h.htlcResolution.ClaimOutpoint)
// This should be non-blocking as we will only attempt to sweep the
// output when the second level tx has already been confirmed. In other
// words, waitHtlcSpendAndCheckPreimage will return immediately.