From 595ee2b688dc1beb22bab7e9b69b890ff3244937 Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 13 Dec 2021 17:18:20 +0100 Subject: [PATCH 1/2] Add payment hash to ClaimHtlcSuccessTx This can be useful to match a preimage to a given claim-htlc-success transaction without going through the process of re-creating the transaction from scratch. We already include the payment hash in htlc-success transactions for that reason. --- .../eclair/transactions/Transactions.scala | 6 ++-- .../channel/version0/ChannelCodecs0.scala | 3 +- .../channel/version0/ChannelTypes0.scala | 4 +-- .../channel/version1/ChannelCodecs1.scala | 2 +- .../channel/version2/ChannelCodecs2.scala | 4 +-- .../channel/version3/ChannelCodecs3.scala | 13 +++++--- .../channel/version3/ChannelCodecs3Spec.scala | 32 +++++++++++++++++-- 7 files changed, 48 insertions(+), 16 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index c8a5160969..8965adeb59 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -125,7 +125,7 @@ object Transactions { case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long) extends HtlcTx { override def desc: String = "htlc-timeout" } case class HtlcDelayedTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-delayed" } sealed trait ClaimHtlcTx extends TransactionWithInputInfo { def htlcId: Long } - case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, htlcId: Long) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } + case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long) extends ClaimHtlcTx { override def desc: String = "claim-htlc-timeout" } sealed trait ClaimAnchorOutputTx extends TransactionWithInputInfo case class ClaimLocalAnchorOutputTx(input: InputInfo, tx: Transaction) extends ClaimAnchorOutputTx with ReplaceableTransactionWithInputInfo { override def desc: String = "local-anchor" } @@ -528,14 +528,14 @@ object Transactions { txIn = TxIn(input.outPoint, ByteVector.empty, sequence) :: Nil, txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, lockTime = 0) - val weight = addSigs(ClaimHtlcSuccessTx(input, tx, htlc.id), PlaceHolderSig, ByteVector32.Zeroes).tx.weight() + val weight = addSigs(ClaimHtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id), PlaceHolderSig, ByteVector32.Zeroes).tx.weight() val fee = weight2fee(feeratePerKw, weight) val amount = input.txOut.amount - fee if (amount < localDustLimit) { Left(AmountBelowDustLimit) } else { val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimHtlcSuccessTx(input, tx1, htlc.id)) + Right(ClaimHtlcSuccessTx(input, tx1, htlc.paymentHash, htlc.id)) } case None => Left(OutputNotFound) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index ce3313fd94..fd6f9c59f7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -32,7 +32,6 @@ import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import java.util.UUID -import scala.concurrent.duration._ /** * Those codecs are here solely for backward compatibility reasons. @@ -137,7 +136,7 @@ private[channel] object ChannelCodecs0 { .typecase(0x01, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) .typecase(0x02, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | provide(0L))).as[HtlcSuccessTx]) .typecase(0x03, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[HtlcTimeoutTx]) - .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[ClaimHtlcSuccessTx]) + .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | provide(ByteVector32.Zeroes)) :: ("htlcId" | provide(0L))).as[ClaimHtlcSuccessTx]) .typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[ClaimHtlcTimeoutTx]) .typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx]) .typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala index b70f8b7430..6aec1441e1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.{Feature, FeatureSupport, Features, channel} +import fr.acinq.eclair.{Feature, Features, channel} import scodec.bits.BitVector private[channel] object ChannelTypes0 { @@ -77,7 +77,7 @@ private[channel] object ChannelTypes0 { // the channel will put a watch at start-up which will make us fetch the spending transaction. val irrevocablySpentNew = irrevocablySpent.collect { case (outpoint, txid) if knownTxs.contains(txid) => (outpoint, knownTxs(txid)) } val claimMainOutputTxNew = claimMainOutputTx.map(tx => ClaimP2WPKHOutputTx(getPartialInputInfo(commitTx, tx), tx)) - val claimHtlcSuccessTxsNew = claimHtlcSuccessTxs.map(tx => ClaimHtlcSuccessTx(getPartialInputInfo(commitTx, tx), tx, 0)) + val claimHtlcSuccessTxsNew = claimHtlcSuccessTxs.map(tx => ClaimHtlcSuccessTx(getPartialInputInfo(commitTx, tx), tx, ByteVector32.Zeroes, 0)) val claimHtlcTimeoutTxsNew = claimHtlcTimeoutTxs.map(tx => ClaimHtlcTimeoutTx(getPartialInputInfo(commitTx, tx), tx, 0)) val claimHtlcTxsNew = (claimHtlcSuccessTxsNew ++ claimHtlcTimeoutTxsNew).map(tx => tx.input.outPoint -> Some(tx)).toMap channel.RemoteCommitPublished(commitTx, claimMainOutputTxNew, claimHtlcTxsNew, Nil, irrevocablySpentNew) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index a68a2e8b63..b1d036df83 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -110,7 +110,7 @@ private[channel] object ChannelCodecs1 { .typecase(0x01, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) .typecase(0x02, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | provide(0L))).as[HtlcSuccessTx]) .typecase(0x03, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[HtlcTimeoutTx]) - .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[ClaimHtlcSuccessTx]) + .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | provide(ByteVector32.Zeroes)) :: ("htlcId" | provide(0L))).as[ClaimHtlcSuccessTx]) .typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[ClaimHtlcTimeoutTx]) .typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx]) .typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 59f05ec348..fe6523e483 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.internal.channel.version2 import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -108,7 +108,7 @@ private[channel] object ChannelCodecs2 { val htlcSuccessTxCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow)).as[HtlcSuccessTx] val htlcTimeoutTxCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[HtlcTimeoutTx] val htlcDelayedTxCodec: Codec[HtlcDelayedTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcDelayedTx] - val claimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[ClaimHtlcSuccessTx] + val claimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | provide(ByteVector32.Zeroes)) :: ("htlcId" | uint64overflow)).as[ClaimHtlcSuccessTx] val claimHtlcTimeoutTxCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[ClaimHtlcTimeoutTx] val claimLocalDelayedOutputTxCodec: Codec[ClaimLocalDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx] val claimP2WPKHOutputTxCodec: Codec[ClaimP2WPKHOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index a22c7055ef..43c891e4d6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.internal.channel.version3 import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -128,7 +128,8 @@ private[channel] object ChannelCodecs3 { val htlcSuccessTxCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow)).as[HtlcSuccessTx] val htlcTimeoutTxCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[HtlcTimeoutTx] val htlcDelayedTxCodec: Codec[HtlcDelayedTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcDelayedTx] - val claimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[ClaimHtlcSuccessTx] + private val legacyClaimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | provide(ByteVector32.Zeroes)) :: ("htlcId" | uint64overflow)).as[ClaimHtlcSuccessTx] + val claimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow)).as[ClaimHtlcSuccessTx] val claimHtlcTimeoutTxCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[ClaimHtlcTimeoutTx] val claimLocalDelayedOutputTxCodec: Codec[ClaimLocalDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx] val claimP2WPKHOutputTxCodec: Codec[ClaimP2WPKHOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx] @@ -144,7 +145,9 @@ private[channel] object ChannelCodecs3 { .typecase(0x01, commitTxCodec) .typecase(0x02, htlcSuccessTxCodec) .typecase(0x03, htlcTimeoutTxCodec) - .typecase(0x04, claimHtlcSuccessTxCodec) + // NB: order matters! + .typecase(0x16, claimHtlcSuccessTxCodec) + .typecase(0x04, legacyClaimHtlcSuccessTxCodec) .typecase(0x05, claimHtlcTimeoutTxCodec) .typecase(0x06, claimP2WPKHOutputTxCodec) .typecase(0x07, claimLocalDelayedOutputTxCodec) @@ -170,7 +173,9 @@ private[channel] object ChannelCodecs3 { .typecase(0x02, htlcTimeoutTxCodec) val claimHtlcTxCodec: Codec[ClaimHtlcTx] = discriminated[ClaimHtlcTx].by(uint8) - .typecase(0x01, claimHtlcSuccessTxCodec) + // NB: order matters! + .typecase(0x03, claimHtlcSuccessTxCodec) + .typecase(0x01, legacyClaimHtlcSuccessTxCodec) .typecase(0x02, claimHtlcTimeoutTxCodec) val htlcTxsAndRemoteSigsCodec: Codec[HtlcTxAndRemoteSig] = ( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index 979b45c18f..38022a49f2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -15,12 +15,13 @@ */ package fr.acinq.eclair.wire.internal.channel.version3 -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.transactions.Transactions.ClaimHtlcSuccessTx import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal -import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs.{DATA_NORMAL_Codec, channelConfigCodec, remoteParamsCodec} +import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs._ import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.stateDataCodec import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, UInt64, randomKey} import org.scalatest.funsuite.AnyFunSuite @@ -124,4 +125,31 @@ class ChannelCodecs3Spec extends AnyFunSuite { assert(decoded1 === decoded2) } + test("backwards compatibility with legacy claim-htlc-success transactions") { + // We can decode old data that didn't contain a payment hash. + val oldTxWithInputInfoCodecBin = hex"0004 24020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd000000002bb0ad010000000000220020e63b4729b67c90212244953d1c9eb15da73dffb035eaef21a5fd883c5968145b8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868fd014302000000000101020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd0000000000ffffffff016297010000000000160014b95a632df5806773f328bb583f973fe5cb05e7a303463043022045676e573cc8314baad6038f2655f106c5cff1d968cd2373ea1a746cb764ffd5021f4c373a06f917f4ed606c9abb3cb9e770c215bc77480c95c6d36e90edea31680120f992c96b4fe64f43cd031c52be0bc39d6c9bf6fc2712a5a9c310dbd58872a9fc8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868000000000000000000000000" + val oldClaimHtlcTxBin = hex"01 24020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd000000002bb0ad010000000000220020e63b4729b67c90212244953d1c9eb15da73dffb035eaef21a5fd883c5968145b8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868fd014302000000000101020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd0000000000ffffffff016297010000000000160014b95a632df5806773f328bb583f973fe5cb05e7a303463043022045676e573cc8314baad6038f2655f106c5cff1d968cd2373ea1a746cb764ffd5021f4c373a06f917f4ed606c9abb3cb9e770c215bc77480c95c6d36e90edea31680120f992c96b4fe64f43cd031c52be0bc39d6c9bf6fc2712a5a9c310dbd58872a9fc8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868000000000000000000000000" + val claimHtlcSuccessWithoutPaymentHash = { + val claimHtlcSuccessTx1 = txWithInputInfoCodec.decode(oldTxWithInputInfoCodecBin.bits).require.value + val claimHtlcSuccessTx2 = claimHtlcTxCodec.decode(oldClaimHtlcTxBin.bits).require.value + assert(claimHtlcSuccessTx1 === claimHtlcSuccessTx2) + assert(claimHtlcSuccessTx1.isInstanceOf[ClaimHtlcSuccessTx]) + assert(claimHtlcSuccessTx1.asInstanceOf[ClaimHtlcSuccessTx].paymentHash === ByteVector32.Zeroes) + claimHtlcSuccessTx1.asInstanceOf[ClaimHtlcSuccessTx] + } + + // We can encode data that contains a payment hash. + val claimHtlcSuccess = claimHtlcSuccessWithoutPaymentHash.copy(paymentHash = ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101")) + val txWithInputInfoCodecBin = txWithInputInfoCodec.encode(claimHtlcSuccess).require.bytes + assert(txWithInputInfoCodecBin !== oldTxWithInputInfoCodecBin) + val claimHtlcTxBin = claimHtlcTxCodec.encode(claimHtlcSuccess).require.bytes + assert(claimHtlcTxBin !== oldClaimHtlcTxBin) + + // And decode new data that contains a payment hash. + val decoded1 = txWithInputInfoCodec.decode(txWithInputInfoCodecBin.bits).require.value + assert(claimHtlcSuccess === decoded1) + val decoded2 = claimHtlcTxCodec.decode(claimHtlcTxBin.bits).require.value + assert(claimHtlcSuccess === decoded2) + } + } From 4dbbca75925f5fad2a71b327f1957535dd4bafed Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 14 Dec 2021 15:58:47 +0100 Subject: [PATCH 2/2] Introduce LegacyClaimHtlcSuccessTx This ensures we handle the case where the payment hash is missing, and the compiler will help us enforce it. --- .../fr/acinq/eclair/transactions/Transactions.scala | 1 + .../internal/channel/version0/ChannelCodecs0.scala | 2 +- .../internal/channel/version0/ChannelTypes0.scala | 2 +- .../internal/channel/version1/ChannelCodecs1.scala | 2 +- .../internal/channel/version2/ChannelCodecs2.scala | 4 ++-- .../internal/channel/version3/ChannelCodecs3.scala | 8 +++----- .../scala/fr/acinq/eclair/channel/HelpersSpec.scala | 1 + .../channel/version3/ChannelCodecs3Spec.scala | 11 +++++------ 8 files changed, 15 insertions(+), 16 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 8965adeb59..ab9e878eff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -125,6 +125,7 @@ object Transactions { case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long) extends HtlcTx { override def desc: String = "htlc-timeout" } case class HtlcDelayedTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-delayed" } sealed trait ClaimHtlcTx extends TransactionWithInputInfo { def htlcId: Long } + case class LegacyClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, htlcId: Long) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long) extends ClaimHtlcTx { override def desc: String = "claim-htlc-timeout" } sealed trait ClaimAnchorOutputTx extends TransactionWithInputInfo diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index fd6f9c59f7..e5ce53cd53 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -136,7 +136,7 @@ private[channel] object ChannelCodecs0 { .typecase(0x01, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) .typecase(0x02, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | provide(0L))).as[HtlcSuccessTx]) .typecase(0x03, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[HtlcTimeoutTx]) - .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | provide(ByteVector32.Zeroes)) :: ("htlcId" | provide(0L))).as[ClaimHtlcSuccessTx]) + .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[LegacyClaimHtlcSuccessTx]) .typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[ClaimHtlcTimeoutTx]) .typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx]) .typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala index 6aec1441e1..b7d193c053 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala @@ -77,7 +77,7 @@ private[channel] object ChannelTypes0 { // the channel will put a watch at start-up which will make us fetch the spending transaction. val irrevocablySpentNew = irrevocablySpent.collect { case (outpoint, txid) if knownTxs.contains(txid) => (outpoint, knownTxs(txid)) } val claimMainOutputTxNew = claimMainOutputTx.map(tx => ClaimP2WPKHOutputTx(getPartialInputInfo(commitTx, tx), tx)) - val claimHtlcSuccessTxsNew = claimHtlcSuccessTxs.map(tx => ClaimHtlcSuccessTx(getPartialInputInfo(commitTx, tx), tx, ByteVector32.Zeroes, 0)) + val claimHtlcSuccessTxsNew = claimHtlcSuccessTxs.map(tx => LegacyClaimHtlcSuccessTx(getPartialInputInfo(commitTx, tx), tx, 0)) val claimHtlcTimeoutTxsNew = claimHtlcTimeoutTxs.map(tx => ClaimHtlcTimeoutTx(getPartialInputInfo(commitTx, tx), tx, 0)) val claimHtlcTxsNew = (claimHtlcSuccessTxsNew ++ claimHtlcTimeoutTxsNew).map(tx => tx.input.outPoint -> Some(tx)).toMap channel.RemoteCommitPublished(commitTx, claimMainOutputTxNew, claimHtlcTxsNew, Nil, irrevocablySpentNew) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index b1d036df83..42415f02ad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -110,7 +110,7 @@ private[channel] object ChannelCodecs1 { .typecase(0x01, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) .typecase(0x02, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | provide(0L))).as[HtlcSuccessTx]) .typecase(0x03, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[HtlcTimeoutTx]) - .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | provide(ByteVector32.Zeroes)) :: ("htlcId" | provide(0L))).as[ClaimHtlcSuccessTx]) + .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[LegacyClaimHtlcSuccessTx]) .typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | provide(0L))).as[ClaimHtlcTimeoutTx]) .typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx]) .typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index fe6523e483..d83dea764d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.internal.channel.version2 import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -108,7 +108,7 @@ private[channel] object ChannelCodecs2 { val htlcSuccessTxCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow)).as[HtlcSuccessTx] val htlcTimeoutTxCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[HtlcTimeoutTx] val htlcDelayedTxCodec: Codec[HtlcDelayedTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcDelayedTx] - val claimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | provide(ByteVector32.Zeroes)) :: ("htlcId" | uint64overflow)).as[ClaimHtlcSuccessTx] + val claimHtlcSuccessTxCodec: Codec[LegacyClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[LegacyClaimHtlcSuccessTx] val claimHtlcTimeoutTxCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[ClaimHtlcTimeoutTx] val claimLocalDelayedOutputTxCodec: Codec[ClaimLocalDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx] val claimP2WPKHOutputTxCodec: Codec[ClaimP2WPKHOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 43c891e4d6..e833daeb01 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -128,7 +128,7 @@ private[channel] object ChannelCodecs3 { val htlcSuccessTxCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow)).as[HtlcSuccessTx] val htlcTimeoutTxCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[HtlcTimeoutTx] val htlcDelayedTxCodec: Codec[HtlcDelayedTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcDelayedTx] - private val legacyClaimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | provide(ByteVector32.Zeroes)) :: ("htlcId" | uint64overflow)).as[ClaimHtlcSuccessTx] + private val legacyClaimHtlcSuccessTxCodec: Codec[LegacyClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[LegacyClaimHtlcSuccessTx] val claimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow)).as[ClaimHtlcSuccessTx] val claimHtlcTimeoutTxCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow)).as[ClaimHtlcTimeoutTx] val claimLocalDelayedOutputTxCodec: Codec[ClaimLocalDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx] @@ -145,8 +145,6 @@ private[channel] object ChannelCodecs3 { .typecase(0x01, commitTxCodec) .typecase(0x02, htlcSuccessTxCodec) .typecase(0x03, htlcTimeoutTxCodec) - // NB: order matters! - .typecase(0x16, claimHtlcSuccessTxCodec) .typecase(0x04, legacyClaimHtlcSuccessTxCodec) .typecase(0x05, claimHtlcTimeoutTxCodec) .typecase(0x06, claimP2WPKHOutputTxCodec) @@ -159,6 +157,7 @@ private[channel] object ChannelCodecs3 { .typecase(0x13, claimRemoteDelayedOutputTxCodec) .typecase(0x14, claimHtlcDelayedOutputPenaltyTxCodec) .typecase(0x15, htlcDelayedTxCodec) + .typecase(0x16, claimHtlcSuccessTxCodec) val claimRemoteCommitMainOutputTxCodec: Codec[ClaimRemoteCommitMainOutputTx] = discriminated[ClaimRemoteCommitMainOutputTx].by(uint8) .typecase(0x01, claimP2WPKHOutputTxCodec) @@ -173,10 +172,9 @@ private[channel] object ChannelCodecs3 { .typecase(0x02, htlcTimeoutTxCodec) val claimHtlcTxCodec: Codec[ClaimHtlcTx] = discriminated[ClaimHtlcTx].by(uint8) - // NB: order matters! - .typecase(0x03, claimHtlcSuccessTxCodec) .typecase(0x01, legacyClaimHtlcSuccessTxCodec) .typecase(0x02, claimHtlcTimeoutTxCodec) + .typecase(0x03, claimHtlcSuccessTxCodec) val htlcTxsAndRemoteSigsCodec: Codec[HtlcTxAndRemoteSig] = ( ("txinfo" | htlcTxCodec) :: diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 6e9edb059b..d3da3d6c6f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -163,6 +163,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat } private def removeHtlcId(claimHtlcTx: ClaimHtlcTx): ClaimHtlcTx = claimHtlcTx match { + case claimHtlcTx: LegacyClaimHtlcSuccessTx => claimHtlcTx.copy(htlcId = 0) case claimHtlcTx: ClaimHtlcSuccessTx => claimHtlcTx.copy(htlcId = 0) case claimHtlcTx: ClaimHtlcTimeoutTx => claimHtlcTx.copy(htlcId = 0) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index 38022a49f2..0355694819 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -19,7 +19,7 @@ import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.transactions.Transactions.ClaimHtlcSuccessTx +import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcSuccessTx, LegacyClaimHtlcSuccessTx} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs._ import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.stateDataCodec @@ -129,17 +129,16 @@ class ChannelCodecs3Spec extends AnyFunSuite { // We can decode old data that didn't contain a payment hash. val oldTxWithInputInfoCodecBin = hex"0004 24020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd000000002bb0ad010000000000220020e63b4729b67c90212244953d1c9eb15da73dffb035eaef21a5fd883c5968145b8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868fd014302000000000101020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd0000000000ffffffff016297010000000000160014b95a632df5806773f328bb583f973fe5cb05e7a303463043022045676e573cc8314baad6038f2655f106c5cff1d968cd2373ea1a746cb764ffd5021f4c373a06f917f4ed606c9abb3cb9e770c215bc77480c95c6d36e90edea31680120f992c96b4fe64f43cd031c52be0bc39d6c9bf6fc2712a5a9c310dbd58872a9fc8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868000000000000000000000000" val oldClaimHtlcTxBin = hex"01 24020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd000000002bb0ad010000000000220020e63b4729b67c90212244953d1c9eb15da73dffb035eaef21a5fd883c5968145b8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868fd014302000000000101020bc3ab58d19af4aac858786c2cbec2e8f9c43a3bd1ce4a51f6b0e1e1b17ffd0000000000ffffffff016297010000000000160014b95a632df5806773f328bb583f973fe5cb05e7a303463043022045676e573cc8314baad6038f2655f106c5cff1d968cd2373ea1a746cb764ffd5021f4c373a06f917f4ed606c9abb3cb9e770c215bc77480c95c6d36e90edea31680120f992c96b4fe64f43cd031c52be0bc39d6c9bf6fc2712a5a9c310dbd58872a9fc8576a914a0fc54aee923e51ceb5d37283b6f263a571cae428763ac672103ea61bef3c6ef05f1e65aaea2020c786e6fc923102c8fe0e53a6fe0315da2e62e7c820120876475527c21023b468606d008f702a9ad940c5fb8539f5b66cf2a3d0a7baa4960377dda9a61d152ae67a91452d220bcd80633ae134ec71b1bd8e79dc1490b9388ac6868000000000000000000000000" - val claimHtlcSuccessWithoutPaymentHash = { + val legacyClaimHtlcSuccess = { val claimHtlcSuccessTx1 = txWithInputInfoCodec.decode(oldTxWithInputInfoCodecBin.bits).require.value val claimHtlcSuccessTx2 = claimHtlcTxCodec.decode(oldClaimHtlcTxBin.bits).require.value assert(claimHtlcSuccessTx1 === claimHtlcSuccessTx2) - assert(claimHtlcSuccessTx1.isInstanceOf[ClaimHtlcSuccessTx]) - assert(claimHtlcSuccessTx1.asInstanceOf[ClaimHtlcSuccessTx].paymentHash === ByteVector32.Zeroes) - claimHtlcSuccessTx1.asInstanceOf[ClaimHtlcSuccessTx] + assert(claimHtlcSuccessTx1.isInstanceOf[LegacyClaimHtlcSuccessTx]) + claimHtlcSuccessTx1.asInstanceOf[LegacyClaimHtlcSuccessTx] } // We can encode data that contains a payment hash. - val claimHtlcSuccess = claimHtlcSuccessWithoutPaymentHash.copy(paymentHash = ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101")) + val claimHtlcSuccess = ClaimHtlcSuccessTx(legacyClaimHtlcSuccess.input, legacyClaimHtlcSuccess.tx, ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101"), legacyClaimHtlcSuccess.htlcId) val txWithInputInfoCodecBin = txWithInputInfoCodec.encode(claimHtlcSuccess).require.bytes assert(txWithInputInfoCodecBin !== oldTxWithInputInfoCodecBin) val claimHtlcTxBin = claimHtlcTxCodec.encode(claimHtlcSuccess).require.bytes