@@ -25,6 +25,7 @@ import fr.acinq.eclair.blockchain.OnChainPubkeyCache
2525import fr .acinq .eclair .blockchain .fee ._
2626import fr .acinq .eclair .channel .fsm .Channel
2727import fr .acinq .eclair .channel .fsm .Channel .REFRESH_CHANNEL_UPDATE_INTERVAL
28+ import fr .acinq .eclair .channel .publish .{ReplaceableClaimHtlcSuccess , ReplaceableHtlcSuccess , TxPublisher }
2829import fr .acinq .eclair .crypto .ShaChain
2930import fr .acinq .eclair .crypto .keymanager .{ChannelKeys , LocalCommitmentKeys , RemoteCommitmentKeys }
3031import fr .acinq .eclair .db .ChannelsDb
@@ -910,9 +911,11 @@ object Helpers {
910911 * Claim the outputs of a local commit tx corresponding to HTLCs. If we don't have the preimage for a received
911912 * * HTLC, we still include an entry in the map because we may receive that preimage later.
912913 */
913- def claimHtlcOutputs (commitKeys : LocalCommitmentKeys , commitment : FullCommitment )(implicit log : LoggingAdapter ): Map [OutPoint , Option [HtlcTx ]] = {
914- // We collect all the preimages we wanted to reveal to our peer.
915- val hash2Preimage : Map [ByteVector32 , ByteVector32 ] = commitment.changes.localChanges.all.collect { case u : UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto .sha256(r) -> r).toMap
914+ private def claimHtlcOutputs (commitKeys : LocalCommitmentKeys , commitment : FullCommitment )(implicit log : LoggingAdapter ): Map [OutPoint , Option [HtlcTx ]] = {
915+ // We collect all the preimages available.
916+ val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect {
917+ case u : UpdateFulfillHtlc => Crypto .sha256(u.paymentPreimage) -> u.paymentPreimage
918+ }.toMap
916919 // We collect incoming HTLCs that we started failing but didn't cross-sign.
917920 val failedIncomingHtlcs : Set [Long ] = commitment.changes.localChanges.all.collect {
918921 case u : UpdateFailHtlc => u.id
@@ -923,9 +926,9 @@ object Helpers {
923926 val nonRelayedIncomingHtlcs : Set [Long ] = commitment.changes.remoteChanges.all.collect { case add : UpdateAddHtlc => add.id }.toSet
924927 commitment.localCommit.htlcTxsAndRemoteSigs.collect {
925928 case HtlcTxAndRemoteSig (txInfo : HtlcSuccessTx , remoteSig) =>
926- if (hash2Preimage .contains(txInfo.paymentHash)) {
929+ if (preimages .contains(txInfo.paymentHash)) {
927930 // We immediately spend incoming htlcs for which we have the preimage.
928- val preimage = hash2Preimage (txInfo.paymentHash)
931+ val preimage = preimages (txInfo.paymentHash)
929932 Some (txInfo.input.outPoint -> withTxGenerationLog(" htlc-success" ) {
930933 val localSig = txInfo.sign(commitKeys, commitment.params.commitmentFormat, Map .empty)
931934 Right (txInfo.addSigs(commitKeys, localSig, remoteSig, preimage, commitment.params.commitmentFormat))
@@ -954,6 +957,45 @@ object Helpers {
954957 }.flatten.toMap
955958 }
956959
960+ /** Claim the outputs of incoming HTLCs for the payment_hash matching the preimage provided. */
961+ def claimHtlcsWithPreimage (commitKeys : LocalCommitmentKeys , localCommitPublished : LocalCommitPublished , commitment : FullCommitment , preimage : ByteVector32 )(implicit log : LoggingAdapter ): (LocalCommitPublished , Seq [TxPublisher .PublishTx ]) = {
962+ val (htlcTxs, toPublish) = commitment.localCommit.htlcTxsAndRemoteSigs.collect {
963+ case HtlcTxAndRemoteSig (txInfo : HtlcSuccessTx , remoteSig) if txInfo.paymentHash == Crypto .sha256(preimage) =>
964+ withTxGenerationLog(" htlc-success" ) {
965+ val localSig = txInfo.sign(commitKeys, commitment.params.commitmentFormat, Map .empty)
966+ Right (txInfo.addSigs(commitKeys, localSig, remoteSig, preimage, commitment.params.commitmentFormat))
967+ }.map(signedTx => {
968+ val toPublish = commitment.params.commitmentFormat match {
969+ case DefaultCommitmentFormat => TxPublisher .PublishFinalTx (signedTx, signedTx.fee, Some (localCommitPublished.commitTx.txid))
970+ case _ : AnchorOutputsCommitmentFormat | _ : SimpleTaprootChannelCommitmentFormat =>
971+ val confirmationTarget = ConfirmationTarget .Absolute (txInfo.htlcExpiry.blockHeight)
972+ TxPublisher .PublishReplaceableTx (ReplaceableHtlcSuccess (signedTx, commitKeys, preimage, remoteSig, localCommitPublished.commitTx, commitment), confirmationTarget)
973+ }
974+ (signedTx, toPublish)
975+ })
976+ }.flatten.unzip
977+ val additionalHtlcTxs = htlcTxs.map(tx => tx.input.outPoint -> Some (tx)).toMap[OutPoint , Option [HtlcTx ]]
978+ val localCommitPublished1 = localCommitPublished.copy(htlcTxs = localCommitPublished.htlcTxs ++ additionalHtlcTxs)
979+ (localCommitPublished1, toPublish)
980+ }
981+
982+ /**
983+ * An incoming HTLC that we've forwarded has been failed downstream: if the channel wasn't closing we would relay
984+ * that failure. Since the channel is closing, our peer should claim the HTLC on-chain after the timeout.
985+ * We stop tracking the corresponding output because we want to move to the CLOSED state even if our peer never
986+ * claims it (which may happen if the HTLC amount is low and on-chain fees are high).
987+ */
988+ def ignoreFailedIncomingHtlc (htlcId : Long , localCommitPublished : LocalCommitPublished , commitment : FullCommitment ): LocalCommitPublished = {
989+ // If we have the preimage (e.g. for partially fulfilled multi-part payments), we keep the HTLC-success tx.
990+ val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect {
991+ case u : UpdateFulfillHtlc => Crypto .sha256(u.paymentPreimage) -> u.paymentPreimage
992+ }.toMap
993+ val outpoints = commitment.localCommit.htlcTxsAndRemoteSigs.collect {
994+ case HtlcTxAndRemoteSig (txInfo : HtlcSuccessTx , _) if txInfo.htlcId == htlcId && ! preimages.contains(txInfo.paymentHash) => txInfo.input.outPoint
995+ }.toSet
996+ localCommitPublished.copy(htlcTxs = localCommitPublished.htlcTxs -- outpoints)
997+ }
998+
957999 /**
9581000 * Claim the output of a 2nd-stage HTLC transaction. If the provided transaction isn't an htlc, this will be a no-op.
9591001 *
@@ -986,7 +1028,7 @@ object Helpers {
9861028 val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
9871029 val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint)
9881030 val mainTx_opt = claimMainOutput(commitment.params, commitKeys, commitTx, feerates, onChainFeeConf, finalScriptPubKey)
989- val htlcTxs = claimHtlcOutputs(channelKeys, commitKeys, commitment, remoteCommit, feerates, finalScriptPubKey)
1031+ val htlcTxs = claimHtlcOutputs(channelKeys, commitKeys, commitment, remoteCommit, finalScriptPubKey)
9901032 val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs
9911033 val anchorTx_opt = if (spendAnchors) {
9921034 claimAnchor(fundingKey, commitKeys, commitTx, commitment.params.commitmentFormat)
@@ -1031,15 +1073,17 @@ object Helpers {
10311073 * Claim the outputs of a remote commit tx corresponding to HTLCs. If we don't have the preimage for a received
10321074 * * HTLC, we still include an entry in the map because we may receive that preimage later.
10331075 */
1034- def claimHtlcOutputs (channelKeys : ChannelKeys , commitKeys : RemoteCommitmentKeys , commitment : FullCommitment , remoteCommit : RemoteCommit , feerates : FeeratesPerKw , finalScriptPubKey : ByteVector )(implicit log : LoggingAdapter ): Map [OutPoint , Option [ClaimHtlcTx ]] = {
1076+ private def claimHtlcOutputs (channelKeys : ChannelKeys , commitKeys : RemoteCommitmentKeys , commitment : FullCommitment , remoteCommit : RemoteCommit , finalScriptPubKey : ByteVector )(implicit log : LoggingAdapter ): Map [OutPoint , Option [ClaimHtlcTx ]] = {
10351077 val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit)
10361078 val remoteCommitTx = makeCommitTx(commitment.commitInput, remoteCommit.index, commitment.params.remoteParams.paymentBasepoint, commitKeys.ourPaymentBasePoint, ! commitment.params.localParams.isChannelOpener, outputs)
10371079 require(remoteCommitTx.tx.txid == remoteCommit.txid, " txid mismatch, cannot recompute the current remote commit tx" )
1038- // We need to use a rather high fee for htlc-claim because we compete with the counterparty .
1039- val feerateHtlc = feerates.fast
1080+ // The feerate will be set by the publisher actor based on the HTLC expiry, we don't care which feerate is used here .
1081+ val feerate = FeeratePerKw ( FeeratePerByte ( 1 sat))
10401082
1041- // We collect all the preimages we wanted to reveal to our peer.
1042- val hash2Preimage : Map [ByteVector32 , ByteVector32 ] = commitment.changes.localChanges.all.collect { case u : UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto .sha256(r) -> r).toMap
1083+ // We collect all the preimages available.
1084+ val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect {
1085+ case u : UpdateFulfillHtlc => Crypto .sha256(u.paymentPreimage) -> u.paymentPreimage
1086+ }.toMap
10431087 // We collect incoming HTLCs that we started failing but didn't cross-sign.
10441088 val failedIncomingHtlcs : Set [Long ] = commitment.changes.localChanges.all.collect {
10451089 case u : UpdateFailHtlc => u.id
@@ -1052,11 +1096,11 @@ object Helpers {
10521096 // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa.
10531097 remoteCommit.spec.htlcs.collect {
10541098 case OutgoingHtlc (add : UpdateAddHtlc ) =>
1055- if (hash2Preimage .contains(add.paymentHash)) {
1099+ if (preimages .contains(add.paymentHash)) {
10561100 // We immediately spend incoming htlcs for which we have the preimage.
1057- val preimage = hash2Preimage (add.paymentHash)
1101+ val preimage = preimages (add.paymentHash)
10581102 withTxGenerationLog(" claim-htlc-success" ) {
1059- ClaimHtlcSuccessTx .createSignedTx(commitKeys, remoteCommitTx.tx, commitment.localParams.dustLimit, outputs, finalScriptPubKey, add, preimage, feerateHtlc , commitment.params.commitmentFormat)
1103+ ClaimHtlcSuccessTx .createSignedTx(commitKeys, remoteCommitTx.tx, commitment.localParams.dustLimit, outputs, finalScriptPubKey, add, preimage, feerate , commitment.params.commitmentFormat)
10601104 }.map(claimHtlcTx => claimHtlcTx.input.outPoint -> Some (claimHtlcTx))
10611105 } else if (failedIncomingHtlcs.contains(add.id)) {
10621106 // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout.
@@ -1076,11 +1120,53 @@ object Helpers {
10761120 // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds
10771121 // back after the timeout.
10781122 withTxGenerationLog(" claim-htlc-timeout" ) {
1079- ClaimHtlcTimeoutTx .createSignedTx(commitKeys, remoteCommitTx.tx, commitment.localParams.dustLimit, outputs, finalScriptPubKey, add, feerateHtlc , commitment.params.commitmentFormat)
1123+ ClaimHtlcTimeoutTx .createSignedTx(commitKeys, remoteCommitTx.tx, commitment.localParams.dustLimit, outputs, finalScriptPubKey, add, feerate , commitment.params.commitmentFormat)
10801124 }.map(claimHtlcTx => claimHtlcTx.input.outPoint -> Some (claimHtlcTx))
10811125 }.flatten.toMap
10821126 }
10831127
1128+ /** Claim the outputs of incoming HTLCs for the payment_hash matching the preimage provided. */
1129+ def claimHtlcsWithPreimage (channelKeys : ChannelKeys , remoteCommitPublished : RemoteCommitPublished , commitment : FullCommitment , remoteCommit : RemoteCommit , preimage : ByteVector32 , finalScriptPubKey : ByteVector )(implicit log : LoggingAdapter ): (RemoteCommitPublished , Seq [TxPublisher .PublishReplaceableTx ]) = {
1130+ val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint)
1131+ val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit)
1132+ // The feerate will be set by the publisher actor based on the HTLC expiry, we don't care which feerate is used here.
1133+ val feerate = FeeratePerKw (FeeratePerByte (1 sat))
1134+ val toPublish = remoteCommit.spec.htlcs.collect {
1135+ // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa.
1136+ case OutgoingHtlc (add : UpdateAddHtlc ) if add.paymentHash == Crypto .sha256(preimage) =>
1137+ withTxGenerationLog(" claim-htlc-success" ) {
1138+ ClaimHtlcSuccessTx .createSignedTx(commitKeys, remoteCommitPublished.commitTx, commitment.localParams.dustLimit, outputs, finalScriptPubKey, add, preimage, feerate, commitment.params.commitmentFormat)
1139+ }.map { signedTx =>
1140+ val confirmationTarget = ConfirmationTarget .Absolute (add.cltvExpiry.blockHeight)
1141+ TxPublisher .PublishReplaceableTx (ReplaceableClaimHtlcSuccess (signedTx, commitKeys, preimage, remoteCommitPublished.commitTx, commitment), confirmationTarget)
1142+ }
1143+ }.flatten.toSeq
1144+ val additionalHtlcTxs = toPublish.map(p => p.input -> Some (p.tx.txInfo.asInstanceOf [ClaimHtlcSuccessTx ])).toMap[OutPoint , Option [ClaimHtlcTx ]]
1145+ val remoteCommitPublished1 = remoteCommitPublished.copy(claimHtlcTxs = remoteCommitPublished.claimHtlcTxs ++ additionalHtlcTxs)
1146+ (remoteCommitPublished1, toPublish)
1147+ }
1148+
1149+ /**
1150+ * An incoming HTLC that we've forwarded has been failed downstream: if the channel wasn't closing we would relay
1151+ * that failure. Since the channel is closing, our peer should claim the HTLC on-chain after the timeout.
1152+ * We stop tracking the corresponding output because we want to move to the CLOSED state even if our peer never
1153+ * claims it (which may happen if the HTLC amount is low and on-chain fees are high).
1154+ */
1155+ def ignoreFailedIncomingHtlc (channelKeys : ChannelKeys , htlcId : Long , remoteCommitPublished : RemoteCommitPublished , commitment : FullCommitment , remoteCommit : RemoteCommit ): RemoteCommitPublished = {
1156+ // If we have the preimage (e.g. for partially fulfilled multi-part payments), we keep the HTLC-success tx.
1157+ val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect {
1158+ case u : UpdateFulfillHtlc => Crypto .sha256(u.paymentPreimage) -> u.paymentPreimage
1159+ }.toMap
1160+ val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint)
1161+ val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit)
1162+ val outpoints = remoteCommit.spec.htlcs.collect {
1163+ // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa.
1164+ case OutgoingHtlc (add : UpdateAddHtlc ) if add.id == htlcId && ! preimages.contains(add.paymentHash) =>
1165+ ClaimHtlcSuccessTx .findInput(remoteCommitPublished.commitTx, outputs, add).map(_.outPoint)
1166+ }.flatten
1167+ remoteCommitPublished.copy(claimHtlcTxs = remoteCommitPublished.claimHtlcTxs -- outpoints)
1168+ }
1169+
10841170 }
10851171
10861172 object RevokedClose {
0 commit comments