-
Notifications
You must be signed in to change notification settings - Fork 24
/
Helpers.kt
1051 lines (960 loc) · 62.5 KB
/
Helpers.kt
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 fr.acinq.lightning.channel
import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.Crypto.ripemd160
import fr.acinq.bitcoin.Crypto.sha256
import fr.acinq.bitcoin.Script.pay2wsh
import fr.acinq.bitcoin.Script.write
import fr.acinq.lightning.Feature
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.NodeParams
import fr.acinq.lightning.blockchain.BITCOIN_OUTPUT_SPENT
import fr.acinq.lightning.blockchain.BITCOIN_TX_CONFIRMED
import fr.acinq.lightning.blockchain.WatchConfirmed
import fr.acinq.lightning.blockchain.WatchSpent
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.blockchain.fee.FeerateTolerance
import fr.acinq.lightning.blockchain.fee.OnChainFeerates
import fr.acinq.lightning.channel.Helpers.Closing.inputsAlreadySpent
import fr.acinq.lightning.crypto.Generators
import fr.acinq.lightning.crypto.KeyManager
import fr.acinq.lightning.transactions.*
import fr.acinq.lightning.transactions.Scripts.multiSig2of2
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx
import fr.acinq.lightning.transactions.Transactions.commitTxFee
import fr.acinq.lightning.transactions.Transactions.makeCommitTxOutputs
import fr.acinq.lightning.utils.*
import fr.acinq.lightning.wire.*
import kotlin.math.max
import kotlin.native.concurrent.ThreadLocal
@ThreadLocal
object Helpers {
val logger by lightningLogger()
/**
* Returns the number of confirmations needed to safely handle the funding transaction,
* we make sure the cumulative block reward largely exceeds the channel size.
*
* @param fundingAmount funding amount of the channel
* @return number of confirmations needed
*/
fun minDepthForFunding(nodeParams: NodeParams, fundingAmount: Satoshi): Int =
if (fundingAmount <= Channel.MAX_FUNDING) {
nodeParams.minDepthBlocks
} else {
val blockReward = 6.25f // this is true as of ~May 2020, but will be too large after 2024
val scalingFactor = 15
val btc = fundingAmount.toLong().toDouble() / 100_000_000L
val blocksToReachFunding: Int = (((scalingFactor * btc) / blockReward) + 1).toInt()
max(nodeParams.minDepthBlocks, blocksToReachFunding)
}
/** Called by the fundee. */
fun validateParamsFundee(nodeParams: NodeParams, open: OpenChannel, channelVersion: ChannelVersion): Either<ChannelException, Unit> {
// BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver:
// MUST reject the channel.
if (nodeParams.chainHash != open.chainHash) {
return Either.Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash))
}
if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis > nodeParams.maxFundingSatoshis) {
return Either.Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, nodeParams.maxFundingSatoshis))
}
// BOLT #2: Channel funding limits
if (open.fundingSatoshis >= Channel.MAX_FUNDING && !nodeParams.features.hasFeature(Feature.Wumbo)) {
return Either.Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING))
}
// BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000.
if (open.pushMsat > open.fundingSatoshis) {
return Either.Left(InvalidPushAmount(open.temporaryChannelId, open.pushMsat, open.fundingSatoshis.toMilliSatoshi()))
}
// BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large.
if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || open.toSelfDelay > nodeParams.maxToLocalDelayBlocks) {
return Either.Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.maxToLocalDelayBlocks))
}
// BOLT #2: The receiving node MUST fail the channel if: max_accepted_htlcs is greater than 483.
if (open.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) {
return Either.Left(InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))
}
// BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000.
if (isFeeTooSmall(open.feeratePerKw)) {
return Either.Left(FeerateTooSmall(open.temporaryChannelId, open.feeratePerKw))
}
if (open.dustLimitSatoshis > nodeParams.maxRemoteDustLimit) {
return Either.Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, nodeParams.maxRemoteDustLimit))
}
if (channelVersion.isSet(ChannelVersion.ZERO_RESERVE_BIT)) {
// in zero-reserve channels, we don't make any requirements on the fundee's reserve (set by the funder in the open_message).
} else {
// BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis.
if (open.dustLimitSatoshis > open.channelReserveSatoshis) {
return Either.Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, open.channelReserveSatoshis))
}
}
// BOLT #2: The receiving node MUST fail the channel if both to_local and to_remote amounts for the initial commitment
// transaction are less than or equal to channel_reserve_satoshis (see BOLT 3).
val (toLocalMsat, toRemoteMsat) = Pair(open.pushMsat, open.fundingSatoshis.toMilliSatoshi() - open.pushMsat)
if (toLocalMsat < open.channelReserveSatoshis && toRemoteMsat < open.channelReserveSatoshis) {
return Either.Left(ChannelReserveNotMet(open.temporaryChannelId, toLocalMsat, toRemoteMsat, open.channelReserveSatoshis))
}
if (isFeeDiffTooHigh(FeeratePerKw.CommitmentFeerate, open.feeratePerKw, nodeParams.onChainFeeConf.feerateTolerance)) {
return Either.Left(FeerateTooDifferent(open.temporaryChannelId, FeeratePerKw.CommitmentFeerate, open.feeratePerKw))
}
// only enforce dust limit check on mainnet
if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash && open.dustLimitSatoshis < Channel.MIN_DUST_LIMIT) {
return Either.Left(DustLimitTooSmall(open.temporaryChannelId, open.dustLimitSatoshis, Channel.MIN_DUST_LIMIT))
}
// we don't check that the funder's amount for the initial commitment transaction is sufficient for full fee payment
// now, but it will be done later when we receive `funding_created`
val reserveToFundingRatio = open.channelReserveSatoshis.toLong().toDouble() / max(open.fundingSatoshis.toLong(), 1)
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) {
return Either.Left(ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio))
}
return Either.Right(Unit)
}
/** Called by the funder. */
fun validateParamsFunder(nodeParams: NodeParams, open: OpenChannel, accept: AcceptChannel): Either<ChannelException, Unit> {
if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) {
return Either.Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))
}
// only enforce dust limit check on mainnet
if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash && accept.dustLimitSatoshis < Channel.MIN_DUST_LIMIT) {
return Either.Left(DustLimitTooSmall(accept.temporaryChannelId, accept.dustLimitSatoshis, Channel.MIN_DUST_LIMIT))
}
if (accept.dustLimitSatoshis > nodeParams.maxRemoteDustLimit) {
return Either.Left(DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, nodeParams.maxRemoteDustLimit))
}
// BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis.
if (accept.dustLimitSatoshis > accept.channelReserveSatoshis) {
return Either.Left(DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, accept.channelReserveSatoshis))
}
// if minimum_depth is unreasonably large: MAY reject the channel.
if (accept.toSelfDelay > Channel.MAX_TO_SELF_DELAY || accept.toSelfDelay > nodeParams.maxToLocalDelayBlocks) {
return Either.Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.maxToLocalDelayBlocks))
}
if ((open.channelVersion ?: ChannelVersion.STANDARD).isSet(ChannelVersion.ZERO_RESERVE_BIT)) {
// in zero-reserve channels, we don't make any requirements on the fundee's reserve (set by the funder in the open_message).
} else {
// if channel_reserve_satoshis from the open_channel message is less than dust_limit_satoshis:
// MUST reject the channel. Other fields have the same requirements as their counterparts in open_channel.
if (open.channelReserveSatoshis < accept.dustLimitSatoshis) {
return Either.Left(DustLimitAboveOurChannelReserve(accept.temporaryChannelId, accept.dustLimitSatoshis, open.channelReserveSatoshis))
}
}
// if channel_reserve_satoshis is less than dust_limit_satoshis within the open_channel message: MUST reject the channel.
if (accept.channelReserveSatoshis < open.dustLimitSatoshis) {
return Either.Left(ChannelReserveBelowOurDustLimit(accept.temporaryChannelId, accept.channelReserveSatoshis, open.dustLimitSatoshis))
}
val reserveToFundingRatio = accept.channelReserveSatoshis.toLong().toDouble() / max(open.fundingSatoshis.toLong(), 1)
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) {
return Either.Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio))
}
return Either.Right(Unit)
}
/**
* @param remoteFeerate remote fee rate per kiloweight
* @return true if the remote fee rate is too small
*/
private fun isFeeTooSmall(remoteFeerate: FeeratePerKw): Boolean = remoteFeerate < FeeratePerKw.MinimumFeeratePerKw
/**
* @param referenceFee reference fee rate per kiloweight
* @param currentFee current fee rate per kiloweight
* @param tolerance maximum fee rate mismatch tolerated
* @return true if the difference between proposed and reference fee rates is too high.
*/
fun isFeeDiffTooHigh(referenceFee: FeeratePerKw, currentFee: FeeratePerKw, tolerance: FeerateTolerance): Boolean =
currentFee < referenceFee * tolerance.ratioLow || referenceFee * tolerance.ratioHigh < currentFee
/**
* This indicates whether our side of the channel is above the reserve requested by our counterparty. In other words,
* this tells if we can use the channel to make a payment.
*/
fun aboveReserve(commitments: Commitments): Boolean {
val remoteCommit = when (commitments.remoteNextCommitInfo) {
is Either.Left -> commitments.remoteNextCommitInfo.value.nextRemoteCommit
else -> commitments.remoteCommit
}
val toRemote = remoteCommit.spec.toRemote.truncateToSatoshi()
// NB: this is an approximation (we don't take network fees into account)
return toRemote > commitments.remoteParams.channelReserve
}
/**
* Tells whether or not their expected next remote commitment number matches with our data
*
* @return
* - true if parties are in sync or remote is behind
* - false if we are behind
*/
fun checkLocalCommit(commitments: Commitments, nextRemoteRevocationNumber: Long): Boolean {
return when {
// they just sent a new commit_sig, we have received it but they didn't receive our revocation
commitments.localCommit.index == nextRemoteRevocationNumber -> true
// we are in sync
commitments.localCommit.index == nextRemoteRevocationNumber + 1 -> true
// remote is behind: we return true because things are fine on our side
commitments.localCommit.index > nextRemoteRevocationNumber + 1 -> true
// we are behind
else -> false
}
}
/**
* Tells whether or not their expected next local commitment number matches with our data
*
* @return
* - true if parties are in sync or remote is behind
* - false if we are behind
*/
fun checkRemoteCommit(commitments: Commitments, nextLocalCommitmentNumber: Long): Boolean {
return when {
commitments.remoteNextCommitInfo.isLeft ->
when {
// we just sent a new commit_sig but they didn't receive it
nextLocalCommitmentNumber == commitments.remoteNextCommitInfo.left!!.nextRemoteCommit.index -> true
// we just sent a new commit_sig, they have received it but we haven't received their revocation
nextLocalCommitmentNumber == (commitments.remoteNextCommitInfo.left!!.nextRemoteCommit.index + 1) -> true
// they are behind
nextLocalCommitmentNumber < commitments.remoteNextCommitInfo.left!!.nextRemoteCommit.index -> true
else -> false
}
commitments.remoteNextCommitInfo.isRight ->
when {
// they have acknowledged the last commit_sig we sent
nextLocalCommitmentNumber == (commitments.remoteCommit.index + 1) -> true
// they are behind
nextLocalCommitmentNumber < (commitments.remoteCommit.index + 1) -> true
else -> false
}
else -> false
}
}
/** This helper method will publish txs only if they haven't yet reached minDepth. */
fun publishIfNeeded(txs: List<Transaction>, irrevocablySpent: Map<OutPoint, Transaction>, channelId: ByteVector32): List<ChannelAction.Blockchain.PublishTx> {
val (skip, process) = txs.partition { it.inputsAlreadySpent(irrevocablySpent) }
skip.forEach { tx -> logger.info { "c:$channelId no need to republish txid=${tx.txid}, it has already been confirmed" } }
return process.map { tx ->
logger.info { "c:$channelId publishing txid=${tx.txid}" }
ChannelAction.Blockchain.PublishTx(tx)
}
}
/** This helper method will watch txs only if they haven't yet reached minDepth. */
fun watchConfirmedIfNeeded(txs: List<Transaction>, irrevocablySpent: Map<OutPoint, Transaction>, channelId: ByteVector32, minDepth: Long): List<ChannelAction.Blockchain.SendWatch> {
val (skip, process) = txs.partition { it.inputsAlreadySpent(irrevocablySpent) }
skip.forEach { tx -> logger.info { "c:$channelId no need to watch txid=${tx.txid}, it has already been confirmed" } }
return process.map { tx -> ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, tx, minDepth, BITCOIN_TX_CONFIRMED(tx))) }
}
/** This helper method will watch txs only if the utxo they spend hasn't already been irrevocably spent. */
fun watchSpentIfNeeded(parentTx: Transaction, outputs: List<OutPoint>, irrevocablySpent: Map<OutPoint, Transaction>, channelId: ByteVector32): List<ChannelAction.Blockchain.SendWatch> {
val (skip, process) = outputs.partition { irrevocablySpent.contains(it) }
skip.forEach { output -> logger.info { "c:$channelId no need to watch output=${output.txid}:${output.index}, it has already been spent by txid=${irrevocablySpent[output]?.txid}" } }
return process.map { output ->
require(output.txid == parentTx.txid) { "output doesn't belong to the given parentTx: txid=${output.txid} but expected txid=${parentTx.txid}" }
ChannelAction.Blockchain.SendWatch(WatchSpent(channelId, parentTx, output.index.toInt(), BITCOIN_OUTPUT_SPENT))
}
}
object Funding {
fun makeFundingInputInfo(
fundingTxId: ByteVector32,
fundingTxOutputIndex: Int,
fundingAmount: Satoshi,
fundingPubkey1: PublicKey,
fundingPubkey2: PublicKey
): Transactions.InputInfo {
val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2)
val fundingTxOut = TxOut(fundingAmount, pay2wsh(fundingScript))
return Transactions.InputInfo(
OutPoint(fundingTxId, fundingTxOutputIndex.toLong()),
fundingTxOut,
ByteVector(write(fundingScript))
)
}
data class FirstCommitTx(val localSpec: CommitmentSpec, val localCommitTx: Transactions.TransactionWithInputInfo.CommitTx, val remoteSpec: CommitmentSpec, val remoteCommitTx: Transactions.TransactionWithInputInfo.CommitTx)
/**
* Creates both sides' first commitment transaction.
*
* @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput)
*/
fun makeFirstCommitTxs(
keyManager: KeyManager,
temporaryChannelId: ByteVector32,
localParams: LocalParams,
remoteParams: RemoteParams,
fundingAmount: Satoshi,
pushMsat: MilliSatoshi,
initialFeerate: FeeratePerKw,
fundingTxHash: ByteVector32,
fundingTxOutputIndex: Int,
remoteFirstPerCommitmentPoint: PublicKey
): Either<ChannelException, FirstCommitTx> {
val toLocalMsat = if (localParams.isFunder) MilliSatoshi(fundingAmount) - pushMsat else pushMsat
val toRemoteMsat = if (localParams.isFunder) pushMsat else MilliSatoshi(fundingAmount) - pushMsat
val localSpec = CommitmentSpec(setOf(), feerate = initialFeerate, toLocal = toLocalMsat, toRemote = toRemoteMsat)
val remoteSpec = CommitmentSpec(setOf(), feerate = initialFeerate, toLocal = toRemoteMsat, toRemote = toLocalMsat)
if (!localParams.isFunder) {
// they are funder, therefore they pay the fee: we need to make sure they can afford it!
val localToRemoteMsat = remoteSpec.toLocal
val fees = commitTxFee(remoteParams.dustLimit, remoteSpec)
val missing = localToRemoteMsat.truncateToSatoshi() - localParams.channelReserve - fees
if (missing < Satoshi(0)) {
return Either.Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.channelReserve, fees = fees))
}
}
val fundingPubKey = localParams.channelKeys.fundingPubKey
val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, fundingAmount, fundingPubKey, remoteParams.fundingPubKey)
val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeys.shaSeed, 0)
val localCommitTx = Commitments.makeLocalTxs(0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec).first
val remoteCommitTx = Commitments.makeRemoteTxs(0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec).first
return Either.Right(FirstCommitTx(localSpec, localCommitTx, remoteSpec, remoteCommitTx))
}
}
object Closing {
// used only to compute tx weights and estimate fees
private val dummyPublicKey by lazy { PrivateKey(ByteArray(32) { 1.toByte() }).publicKey() }
private fun isValidFinalScriptPubkey(scriptPubKey: ByteArray): Boolean {
return runTrying {
val script = Script.parse(scriptPubKey)
Script.isPay2pkh(script) || Script.isPay2sh(script) || Script.isPay2wpkh(script) || Script.isPay2wsh(script)
}.getOrElse { false }
}
fun isValidFinalScriptPubkey(scriptPubKey: ByteVector): Boolean = isValidFinalScriptPubkey(scriptPubKey.toByteArray())
// To be replaced with corresponding function in bitcoin-kmp
fun btcAddressFromScriptPubKey(scriptPubKey: ByteVector, chainHash: ByteVector32): String? {
return runTrying {
val script = Script.parse(scriptPubKey)
when {
Script.isPay2pkh(script) -> {
// OP_DUP OP_HASH160 OP_PUSHDATA(20) OP_EQUALVERIFY OP_CHECKSIG
val opPushData = script[2] as OP_PUSHDATA
val prefix = when (chainHash) {
Block.LivenetGenesisBlock.hash -> Base58.Prefix.PubkeyAddress
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash -> Base58.Prefix.PubkeyAddressTestnet
else -> null
} ?: return null
Base58Check.encode(prefix, opPushData.data)
}
Script.isPay2sh(script) -> {
// OP_HASH160 OP_PUSHDATA(20) OP_EQUAL
val opPushData = script[1] as OP_PUSHDATA
val prefix = when (chainHash) {
Block.LivenetGenesisBlock.hash -> Base58.Prefix.ScriptAddress
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash -> Base58.Prefix.ScriptAddressTestnet
else -> null
} ?: return null
Base58Check.encode(prefix, opPushData.data)
}
Script.isPay2wpkh(script) || Script.isPay2wsh(script) -> {
// isPay2wpkh : OP_0 OP_PUSHDATA(20)
// isPay2wsh : OP_0 OP_PUSHDATA(32)
val opPushData = script[1] as OP_PUSHDATA
val hrp = when (chainHash) {
Block.LivenetGenesisBlock.hash -> "bc"
Block.TestnetGenesisBlock.hash -> "tb"
Block.RegtestGenesisBlock.hash -> "bcrt"
else -> null
} ?: return null
Bech32.encodeWitnessAddress(hrp, 0, opPushData.data.toByteArray())
}
else -> null
} // </when>
}.getOrElse { null }
}
fun firstClosingFee(commitments: Commitments, localScriptPubkey: ByteArray, remoteScriptPubkey: ByteArray, requestedFeerate: FeeratePerKw): Satoshi {
// this is just to estimate the weight which depends on the size of the pubkey scripts
val dummyClosingTx = Transactions.makeClosingTx(commitments.commitInput, localScriptPubkey, remoteScriptPubkey, commitments.localParams.isFunder, Satoshi(0), Satoshi(0), commitments.localCommit.spec)
val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, commitments.remoteParams.fundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx)
return Transactions.weight2fee(requestedFeerate, closingWeight)
}
fun firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, requestedFeerate: FeeratePerKw): Satoshi =
firstClosingFee(commitments, localScriptPubkey.toByteArray(), remoteScriptPubkey.toByteArray(), requestedFeerate)
fun nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2
fun makeFirstClosingTx(
keyManager: KeyManager,
commitments: Commitments,
localScriptPubkey: ByteArray,
remoteScriptPubkey: ByteArray,
requestedFeerate: FeeratePerKw
): Pair<Transactions.TransactionWithInputInfo.ClosingTx, ClosingSigned> {
val closingFee = firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, requestedFeerate)
return makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFee)
}
fun makeClosingTx(
keyManager: KeyManager,
commitments: Commitments,
localScriptPubkey: ByteArray,
remoteScriptPubkey: ByteArray,
closingFee: Satoshi
): Pair<Transactions.TransactionWithInputInfo.ClosingTx, ClosingSigned> {
require(isValidFinalScriptPubkey(localScriptPubkey)) { "invalid localScriptPubkey" }
require(isValidFinalScriptPubkey(remoteScriptPubkey)) { "invalid remoteScriptPubkey" }
val dustLimit = commitments.localParams.dustLimit.max(commitments.remoteParams.dustLimit)
val closingTx = Transactions.makeClosingTx(commitments.commitInput, localScriptPubkey, remoteScriptPubkey, commitments.localParams.isFunder, dustLimit, closingFee, commitments.localCommit.spec)
val localClosingSig = keyManager.sign(closingTx, commitments.localParams.channelKeys.fundingPrivateKey)
val closingSigned = ClosingSigned(commitments.channelId, closingFee, localClosingSig)
return Pair(closingTx, closingSigned)
}
fun checkClosingSignature(
keyManager: KeyManager,
commitments: Commitments,
localScriptPubkey: ByteArray,
remoteScriptPubkey: ByteArray,
remoteClosingFee: Satoshi,
remoteClosingSig: ByteVector64
): Either<ChannelException, ClosingTx> {
val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee)
return if (checkClosingDustAmounts(closingTx)) {
val signedClosingTx = Transactions.addSigs(closingTx, commitments.localParams.channelKeys.fundingPubKey, commitments.remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig)
when (Transactions.checkSpendable(signedClosingTx)) {
is Try.Success -> Either.Right(signedClosingTx)
is Try.Failure -> Either.Left(InvalidCloseSignature(commitments.channelId, signedClosingTx.tx))
}
} else {
Either.Left(InvalidCloseAmountBelowDust(commitments.channelId, closingTx.tx))
}
}
/**
* Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk
* that the closing transaction will not be relayed to miners' mempool and will not confirm.
* The various dust limits are detailed in https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#dust-limits
*/
fun checkClosingDustAmounts(closingTx: ClosingTx): Boolean {
return closingTx.tx.txOut.all { txOut ->
val publicKeyScript = txOut.publicKeyScript.toByteArray()
when {
Script.isPay2pkh(publicKeyScript) -> txOut.amount >= 546.sat
Script.isPay2sh(publicKeyScript) -> txOut.amount >= 540.sat
Script.isPay2wpkh(publicKeyScript) -> txOut.amount >= 294.sat
Script.isPay2wsh(publicKeyScript) -> txOut.amount >= 330.sat
else -> false
}
}
}
/**
* Claim all the outputs that we've received from our current commit tx. This will be done using 2nd stage HTLC transactions.
*
* @param commitments our commitment data, which include payment preimages.
* @return a list of transactions (one per output that we can claim).
*/
fun claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, feerates: OnChainFeerates): LocalCommitPublished {
val localCommit = commitments.localCommit
val localParams = commitments.localParams
require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid) { "txid mismatch, provided tx is not the current local commit tx" }
val localPerCommitmentPoint = keyManager.commitmentPoint(commitments.localParams.channelKeys.shaSeed, commitments.localCommit.index)
val localRevocationPubkey = Generators.revocationPubKey(commitments.remoteParams.revocationBasepoint, localPerCommitmentPoint)
val localDelayedPubkey = Generators.derivePubKey(localParams.channelKeys.delayedPaymentBasepoint, localPerCommitmentPoint)
val feerateDelayed = feerates.claimMainFeerate
// first we will claim our main output as soon as the delay is over
val mainDelayedTx = generateTx("main-delayed-output") {
Transactions.makeClaimLocalDelayedOutputTx(
tx,
localParams.dustLimit,
localRevocationPubkey,
commitments.remoteParams.toSelfDelay,
localDelayedPubkey,
localParams.defaultFinalScriptPubKey.toByteArray(),
feerateDelayed
)
}?.let {
val sig = keyManager.sign(it, localParams.channelKeys.delayedPaymentKey, localPerCommitmentPoint, SigHash.SIGHASH_ALL)
Transactions.addSigs(it, sig)
}
// those are the preimages to existing received htlcs
val preimages = commitments.localChanges.all.filterIsInstance<UpdateFulfillHtlc>().map { it.paymentPreimage }
val htlcTxs = localCommit.publishableTxs.htlcTxsAndSigs.map { (txInfo, localSig, remoteSig) ->
when (txInfo) {
is HtlcSuccessTx -> when (val preimage = preimages.firstOrNull { r -> r.sha256() == txInfo.paymentHash }) {
// incoming htlc for which we don't have the preimage: we can't spend it immediately, but we may learn the
// preimage later, otherwise it will eventually timeout and they will get their funds back
null -> Pair(txInfo.input.outPoint, null)
// incoming htlc for which we have the preimage: we can spend it directly
else -> Pair(txInfo.input.outPoint, Transactions.addSigs(txInfo, localSig, remoteSig, preimage))
}
// outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout
is HtlcTimeoutTx -> Pair(txInfo.input.outPoint, Transactions.addSigs(txInfo, localSig, remoteSig))
}
}.toMap()
// all htlc output to us are delayed, so we need to claim them as soon as the delay is over
val htlcDelayedTxs = htlcTxs.values.filterNotNull().mapNotNull { txInfo ->
generateTx("claim-htlc-delayed") {
Transactions.makeClaimLocalDelayedOutputTx(
txInfo.tx,
localParams.dustLimit,
localRevocationPubkey,
commitments.remoteParams.toSelfDelay,
localDelayedPubkey,
localParams.defaultFinalScriptPubKey.toByteArray(),
feerateDelayed
)
}?.let {
val sig = keyManager.sign(it, localParams.channelKeys.delayedPaymentKey, localPerCommitmentPoint, SigHash.SIGHASH_ALL)
Transactions.addSigs(it, sig)
}
}
return LocalCommitPublished(
commitTx = tx,
claimMainDelayedOutputTx = mainDelayedTx,
htlcTxs = htlcTxs,
claimHtlcDelayedTxs = htlcDelayedTxs,
claimAnchorTxs = emptyList(),
irrevocablySpent = emptyMap()
)
}
/**
* Claim all the outputs that we've received from their current commit tx.
*
* @param commitments our commitment data, which include payment preimages.
* @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment).
* @param tx the remote commitment transaction that has just been published.
* @return a list of transactions (one per output that we can claim).
*/
fun claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, feerates: OnChainFeerates): RemoteCommitPublished {
val channelVersion = commitments.channelVersion
val localParams = commitments.localParams
val remoteParams = commitments.remoteParams
val commitInput = commitments.commitInput
val (remoteCommitTx, _) = Commitments.makeRemoteTxs(
remoteCommit.index,
localParams,
remoteParams,
commitInput,
remoteCommit.remotePerCommitmentPoint,
remoteCommit.spec
)
require(remoteCommitTx.tx.txid == tx.txid) { "txid mismatch, provided tx is not the current remote commit tx" }
val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion)
val localPaymentPubkey = keyManager.paymentPoint(channelKeyPath).publicKey
val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint)
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint)
val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint)
val outputs = makeCommitTxOutputs(
commitments.remoteParams.fundingPubKey,
commitments.localParams.channelKeys.fundingPubKey,
!localParams.isFunder,
remoteParams.dustLimit,
remoteRevocationPubkey,
localParams.toSelfDelay,
remoteDelayedPaymentPubkey,
localPaymentPubkey,
remoteHtlcPubkey,
localHtlcPubkey,
remoteCommit.spec
)
// we need to use a rather high fee for htlc-claim because we compete with the counterparty
val feerateClaimHtlc = feerates.fastFeerate
// those are the preimages to existing received htlcs
val preimages = commitments.localChanges.all.filterIsInstance<UpdateFulfillHtlc>().map { it.paymentPreimage }
// remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa
val claimHtlcTxs = remoteCommit.spec.htlcs.mapNotNull { htlc ->
when (htlc) {
is OutgoingHtlc -> {
generateTx("claim-htlc-success") {
Transactions.makeClaimHtlcSuccessTx(
remoteCommitTx.tx,
outputs,
localParams.dustLimit,
localHtlcPubkey,
remoteHtlcPubkey,
remoteRevocationPubkey,
localParams.defaultFinalScriptPubKey.toByteArray(),
htlc.add,
feerateClaimHtlc
)
}?.let { claimHtlcTx ->
when (val preimage = preimages.firstOrNull { r -> r.sha256() == htlc.add.paymentHash }) {
// incoming htlc for which we don't have the preimage: we can't spend it immediately, but we may learn the
// preimage later, otherwise it will eventually timeout and they will get their funds back
null -> Pair(claimHtlcTx.input.outPoint, null)
// incoming htlc for which we have the preimage: we can spend it directly
else -> {
val sig = keyManager.sign(claimHtlcTx, localParams.channelKeys.htlcKey, remoteCommit.remotePerCommitmentPoint, SigHash.SIGHASH_ALL)
Pair(claimHtlcTx.input.outPoint, Transactions.addSigs(claimHtlcTx, sig, preimage))
}
}
}
}
is IncomingHtlc -> {
// outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout
generateTx("claim-htlc-timeout") {
Transactions.makeClaimHtlcTimeoutTx(
remoteCommitTx.tx,
outputs,
localParams.dustLimit,
localHtlcPubkey,
remoteHtlcPubkey,
remoteRevocationPubkey,
localParams.defaultFinalScriptPubKey.toByteArray(),
htlc.add,
feerateClaimHtlc
)
}?.let { claimHtlcTx ->
val sig = keyManager.sign(claimHtlcTx, localParams.channelKeys.htlcKey, remoteCommit.remotePerCommitmentPoint, SigHash.SIGHASH_ALL)
Pair(claimHtlcTx.input.outPoint, Transactions.addSigs(claimHtlcTx, sig))
}
}
}
}.toMap()
// we claim our output and add the htlc txs we just created
return claimRemoteCommitMainOutput(keyManager, commitments, tx, feerates.claimMainFeerate).copy(claimHtlcTxs = claimHtlcTxs)
}
/**
* Claim our main output only from their commit tx.
*
* @param commitments either our current commitment data in case of usual remote uncooperative closing or our outdated commitment data
* in case of data loss protection procedure; in any case it is used only to get some constant parameters, not commitment data.
* @param tx the remote commitment transaction that has just been published.
* @return a transaction to claim our main output.
*/
internal fun claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, tx: Transaction, claimMainFeerate: FeeratePerKw): RemoteCommitPublished {
val localPaymentPoint = commitments.localParams.channelKeys.paymentBasepoint
val mainTx = generateTx("claim-remote-delayed-output") {
Transactions.makeClaimRemoteDelayedOutputTx(
tx,
commitments.localParams.dustLimit,
localPaymentPoint,
commitments.localParams.defaultFinalScriptPubKey,
claimMainFeerate
)
}?.let {
val sig = keyManager.sign(it, commitments.localParams.channelKeys.paymentKey)
Transactions.addSigs(it, sig)
}
return RemoteCommitPublished(commitTx = tx, claimMainOutputTx = mainTx)
}
/**
* When an unexpected transaction spending the funding tx is detected, we can use our secrets to identify the commitment number.
* This can then be used to find the necessary information to build penalty txs for every htlc output.
*/
private fun extractTxNumber(keyManager: KeyManager, commitments: Commitments, tx: Transaction): Long {
require(tx.txIn.size == 1) { "commitment tx should have 1 input" }
val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion)
val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn.first().sequence, tx.lockTime)
val localPaymentPoint = keyManager.paymentPoint(channelKeyPath)
// this tx has been published by remote, so we need to invert local/remote params
val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !commitments.localParams.isFunder, commitments.remoteParams.paymentBasepoint, localPaymentPoint.publicKey)
require(txNumber <= 0xffffffffffffL) { "txNumber must be lesser than 48 bits long" }
logger.warning { "c:${commitments.channelId} a revoked commit has been published with txNumber=$txNumber" }
return txNumber
}
/**
* When an unexpected transaction spending the funding tx is detected:
* 1) we find out if the published transaction is one of our remote's revoked txs
* 2) and then:
* a) if it is a revoked tx we build a set of transactions that will punish them by stealing all their funds
* b) otherwise there is nothing we can do
*
* @return a [[RevokedCommitPublished]] object containing a penalty transaction for the remote's main output and the commitment number.
* With the commitment number, the caller should fetch information about the htlcs in this commitment and then call [[claimRevokedRemoteCommitTxHtlcOutputs]].
*/
fun claimRevokedRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, feerates: OnChainFeerates): Pair<RevokedCommitPublished, Long>? {
val txNumber = extractTxNumber(keyManager, commitments, tx)
// now we know what commit number this tx is referring to, we can derive the commitment point from the shachain
val hash = commitments.remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) ?: return null
val localPaymentPoint = commitments.localParams.channelKeys.paymentBasepoint
val remotePerCommitmentSecret = PrivateKey.fromHex(hash.toHex())
val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey()
val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitments.remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
val remoteRevocationPubkey = Generators.revocationPubKey(commitments.localParams.channelKeys.revocationBasepoint, remotePerCommitmentPoint)
val feerateMain = feerates.claimMainFeerate
// we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty
val feeratePenalty = feerates.fastFeerate
// first we will claim our main output right away
val mainTx = generateTx("claim-remote-delayed-output") {
Transactions.makeClaimRemoteDelayedOutputTx(
tx,
commitments.localParams.dustLimit,
localPaymentPoint,
commitments.localParams.defaultFinalScriptPubKey,
feerateMain
)
}?.let {
val sig = keyManager.sign(it, commitments.localParams.channelKeys.paymentKey)
Transactions.addSigs(it, sig)
}
// then we punish them by stealing their main output
val mainPenaltyTx = generateTx("main-penalty") {
Transactions.makeMainPenaltyTx(
tx,
commitments.localParams.dustLimit,
remoteRevocationPubkey,
commitments.localParams.defaultFinalScriptPubKey.toByteArray(),
commitments.localParams.toSelfDelay,
remoteDelayedPaymentPubkey,
feeratePenalty
)
}?.let {
val sig = keyManager.sign(it, commitments.localParams.channelKeys.revocationKey, remotePerCommitmentSecret)
Transactions.addSigs(it, sig)
}
return Pair(RevokedCommitPublished(commitTx = tx, remotePerCommitmentSecret = remotePerCommitmentSecret, claimMainOutputTx = mainTx, mainPenaltyTx = mainPenaltyTx), txNumber)
}
/**
* Once we've fetched htlc information for a revoked commitment from the DB, we create penalty transactions to claim all htlc outputs.
*/
fun claimRevokedRemoteCommitTxHtlcOutputs(
keyManager: KeyManager,
commitments: Commitments,
revokedCommitPublished: RevokedCommitPublished,
feerates: OnChainFeerates,
htlcInfos: List<ChannelAction.Storage.HtlcInfo>
): RevokedCommitPublished {
// we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty
val feeratePenalty = feerates.fastFeerate
val remotePerCommitmentPoint = revokedCommitPublished.remotePerCommitmentSecret.publicKey()
val remoteRevocationPubkey = Generators.revocationPubKey(commitments.localParams.channelKeys.revocationBasepoint, remotePerCommitmentPoint)
val remoteHtlcPubkey = Generators.derivePubKey(commitments.remoteParams.htlcBasepoint, remotePerCommitmentPoint)
val localHtlcPubkey = Generators.derivePubKey(commitments.localParams.channelKeys.htlcBasepoint, remotePerCommitmentPoint)
// we retrieve the information needed to rebuild htlc scripts
logger.info { "c:${commitments.channelId} found ${htlcInfos.size} htlcs for txid=${revokedCommitPublished.commitTx.txid}" }
val htlcsRedeemScripts = htlcInfos.flatMap { htlcInfo ->
val htlcReceived = Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash), htlcInfo.cltvExpiry)
val htlcOffered = Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash))
listOf(htlcReceived, htlcOffered)
}.map { redeemScript -> write(pay2wsh(redeemScript)).toByteVector() to write(redeemScript).toByteVector() }.toMap()
// and finally we steal the htlc outputs
val htlcPenaltyTxs = revokedCommitPublished.commitTx.txOut.mapIndexedNotNull { outputIndex, txOut ->
htlcsRedeemScripts[txOut.publicKeyScript]?.let { redeemScript ->
generateTx("htlc-penalty") {
Transactions.makeHtlcPenaltyTx(
revokedCommitPublished.commitTx,
outputIndex,
redeemScript.toByteArray(),
commitments.localParams.dustLimit,
commitments.localParams.defaultFinalScriptPubKey.toByteArray(),
feeratePenalty
)
}?.let { htlcPenaltyTx ->
val sig = keyManager.sign(htlcPenaltyTx, commitments.localParams.channelKeys.revocationKey, revokedCommitPublished.remotePerCommitmentSecret)
Transactions.addSigs(htlcPenaltyTx, sig, remoteRevocationPubkey)
}
}
}
return revokedCommitPublished.copy(htlcPenaltyTxs = htlcPenaltyTxs)
}
/**
* Claims the output of an [[HtlcSuccessTx]] or [[HtlcTimeoutTx]] transaction using a revocation key.
*
* In case a revoked commitment with pending HTLCs is published, there are two ways the HTLC outputs can be taken as punishment:
* - by spending the corresponding output of the commitment tx, using [[ClaimHtlcDelayedOutputPenaltyTx]] that we generate as soon as we detect that a revoked commit
* has been spent; note that those transactions will compete with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] published by the counterparty.
* - by spending the delayed output of [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] if those get confirmed; because the output of these txs is protected by
* an OP_CSV delay, we will have time to spend them with a revocation key. In that case, we generate the spending transactions "on demand",
* this is the purpose of this method.
*
* NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same
* lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs.
*/
fun claimRevokedHtlcTxOutputs(
keyManager: KeyManager,
commitments: Commitments,
revokedCommitPublished: RevokedCommitPublished,
htlcTx: Transaction,
feerates: OnChainFeerates
): Pair<RevokedCommitPublished, List<ClaimHtlcDelayedOutputPenaltyTx>> {
val claimTxs = buildList {
revokedCommitPublished.claimMainOutputTx?.let { add(it) }
revokedCommitPublished.mainPenaltyTx?.let { add(it) }
addAll(revokedCommitPublished.htlcPenaltyTxs)
}
val isHtlcTx = htlcTx.txIn.any { it.outPoint.txid == revokedCommitPublished.commitTx.txid } && !claimTxs.any { it.tx.txid == htlcTx.txid }
if (isHtlcTx) {
logger.info { "c:${commitments.channelId} looks like txid=${htlcTx.txid} could be a 2nd level htlc tx spending revoked commit txid=${revokedCommitPublished.commitTx.txid}" }
// Let's assume that htlcTx is an HtlcSuccessTx or HtlcTimeoutTx and try to generate a tx spending its output using a revocation key
val remotePerCommitmentPoint = revokedCommitPublished.remotePerCommitmentSecret.publicKey()
val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitments.remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
val remoteRevocationPubkey = Generators.revocationPubKey(commitments.localParams.channelKeys.revocationBasepoint, remotePerCommitmentPoint)
// we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty
val feeratePenalty = feerates.fastFeerate
val penaltyTxs = Transactions.makeClaimDelayedOutputPenaltyTxs(
htlcTx,
commitments.localParams.dustLimit,
remoteRevocationPubkey,
commitments.localParams.toSelfDelay,
remoteDelayedPaymentPubkey,
commitments.localParams.defaultFinalScriptPubKey.toByteArray(),
feeratePenalty
).mapNotNull { claimDelayedOutputPenaltyTx ->
generateTx("claim-htlc-delayed-penalty") {
claimDelayedOutputPenaltyTx
}?.let {
val sig = keyManager.sign(it, commitments.localParams.channelKeys.revocationKey, revokedCommitPublished.remotePerCommitmentSecret)
val signedTx = Transactions.addSigs(it, sig)
// we need to make sure that the tx is indeed valid
when (runTrying { Transaction.correctlySpends(signedTx.tx, listOf(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) {
is Try.Success -> signedTx
is Try.Failure -> null
}
}
}
return revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs + penaltyTxs) to penaltyTxs
} else {
return revokedCommitPublished to listOf()
}
}
/**
* In CLOSING state, any time we see a new transaction, we try to extract a preimage from it in order to fulfill the
* corresponding incoming htlc in an upstream channel.
*
* Not doing that would result in us losing money, because the downstream node would pull money from one side, and
* the upstream node would get refunded after a timeout.
*
* @return a set of pairs (add, preimage) if extraction was successful:
* - add is the htlc in the downstream channel from which we extracted the preimage
* - preimage needs to be sent to the upstream channel
*/
fun LocalCommit.extractPreimages(tx: Transaction): Set<Pair<UpdateAddHtlc, ByteVector32>> {
val htlcSuccess = tx.txIn.map { it.witness }.mapNotNull(Scripts.extractPreimageFromHtlcSuccess())
.onEach { logger.info { "extracted paymentPreimage=$it from tx=$tx (htlc-success)" } }
val claimHtlcSuccess = tx.txIn.map { it.witness }.mapNotNull(Scripts.extractPreimageFromClaimHtlcSuccess())
.onEach { logger.info { "extracted paymentPreimage=$it from tx=$tx (claim-htlc-success)" } }
val paymentPreimages = (htlcSuccess + claimHtlcSuccess).toSet()
return paymentPreimages.flatMap { paymentPreimage ->
// we only consider htlcs in our local commitment, because we only care about outgoing htlcs, which disappear first in the remote commitment
// if an outgoing htlc is in the remote commitment, then:
// - either it is in the local commitment (it was never fulfilled)
// - or we have already received the fulfill and forwarded it upstream
spec.htlcs.filter { it is OutgoingHtlc && it.add.paymentHash.contentEquals(sha256(paymentPreimage)) }.map { it.add to paymentPreimage }
}.toSet()
}
/**
* In CLOSING state, when we are notified that a transaction has been confirmed, we analyze it to find out if one or
* more htlcs have timed out and need to be failed in an upstream channel.
*
* @param tx a tx that has reached min_depth
* @return a set of htlcs that need to be failed upstream
*/
fun LocalCommit.timedOutHtlcs(localCommitPublished: LocalCommitPublished, localDustLimit: Satoshi, tx: Transaction): Set<UpdateAddHtlc> {
val untrimmedHtlcs = Transactions.trimOfferedHtlcs(localDustLimit, spec).map { it.add }
return when {
tx.txid == publishableTxs.commitTx.tx.txid -> {
// the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx)
(spec.htlcs.outgoings() - untrimmedHtlcs).toSet()
}
localCommitPublished.isHtlcTimeout(tx) -> {
// maybe this is a timeout tx, in that case we can resolve and fail the corresponding htlc
tx.txIn.mapNotNull { txIn ->
when (val htlcTx = localCommitPublished.htlcTxs[txIn.outPoint]) {
is HtlcTimeoutTx -> when (val htlc = untrimmedHtlcs.find { it.id == htlcTx.htlcId }) {
null -> {
logger.error { "could not find htlc #${htlcTx.htlcId} for htlc-timeout tx=$tx" }
null
}
else -> {
logger.info { "htlc-timeout tx for htlc #${htlc.id} paymentHash=${htlc.paymentHash} expiry=${tx.lockTime} has been confirmed (tx=$tx)" }
htlc
}
}
else -> null
}
}.toSet()
}
else -> emptySet()
}
}
/**
* In CLOSING state, when we are notified that a transaction has been confirmed, we analyze it to find out if one or
* more htlcs have timed out and need to be failed in an upstream channel.
*
* @param tx a tx that has reached min_depth
* @return a set of htlcs that need to be failed upstream
*/
fun RemoteCommit.timedOutHtlcs(remoteCommitPublished: RemoteCommitPublished, remoteDustLimit: Satoshi, tx: Transaction): Set<UpdateAddHtlc> {
val untrimmedHtlcs = Transactions.trimReceivedHtlcs(remoteDustLimit, spec).map { it.add }
return when {
tx.txid == txid -> {
// the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx)
(spec.htlcs.incomings() - untrimmedHtlcs).toSet()
}
remoteCommitPublished.isClaimHtlcTimeout(tx) -> {
// maybe this is a timeout tx, in that case we can resolve and fail the corresponding htlc
tx.txIn.mapNotNull { txIn ->
when (val htlcTx = remoteCommitPublished.claimHtlcTxs[txIn.outPoint]) {
is ClaimHtlcTimeoutTx -> when (val htlc = untrimmedHtlcs.find { it.id == htlcTx.htlcId }) {
null -> {
logger.error { "could not find htlc #${htlcTx.htlcId} for claim-htlc-timeout tx=$tx" }
null
}
else -> {
logger.info { "claim-htlc-timeout tx for htlc #${htlc.id} paymentHash=${htlc.paymentHash} expiry=${tx.lockTime} has been confirmed (tx=$tx)" }
htlc
}
}
else -> null
}
}.toSet()
}
else -> emptySet()
}
}
/**
* As soon as a local or remote commitment reaches min_depth, we know which htlcs will be settled on-chain (whether
* or not they actually have an output in the commitment tx).
*
* @param tx a transaction that is sufficiently buried in the blockchain
*/
fun onChainOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: RemoteCommit?, tx: Transaction): Set<UpdateAddHtlc> = when {
localCommit.publishableTxs.commitTx.tx.txid == tx.txid -> localCommit.spec.htlcs.outgoings().toSet()
remoteCommit.txid == tx.txid -> remoteCommit.spec.htlcs.incomings().toSet()
nextRemoteCommit_opt?.txid == tx.txid -> nextRemoteCommit_opt.spec.htlcs.incomings().toSet()
else -> emptySet()
}
/**
* If a commitment tx reaches min_depth, we need to fail the outgoing htlcs that will never reach the blockchain.
* It could be because only us had signed them, or because a revoked commitment got confirmed.
*/
fun overriddenOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit: RemoteCommit?, revokedCommitPublished: List<RevokedCommitPublished>, tx: Transaction): Set<UpdateAddHtlc> = when {
localCommit.publishableTxs.commitTx.tx.txid == tx.txid -> {
// our commit got confirmed, so any htlc that is in their commitment but not in ours will never reach the chain
val htlcsInRemoteCommit = remoteCommit.spec.htlcs + nextRemoteCommit?.spec?.htlcs.orEmpty()
// NB: from the point of view of the remote, their incoming htlcs are our outgoing htlcs
htlcsInRemoteCommit.incomings().toSet() - localCommit.spec.htlcs.outgoings().toSet()
}
remoteCommit.txid == tx.txid -> when (nextRemoteCommit) {
null -> emptySet() // their last commitment got confirmed, so no htlcs will be overridden, they will timeout or be fulfilled on chain
else -> {