From c34f51274511670fd1f705cb964af65eb0cb403b Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 1 Sep 2021 10:45:49 +0200 Subject: [PATCH 1/3] Add anchor outputs zero fee htlcs feature bit Create the corresponding feature bit and channel type. Ensure it can be correctly negotiated and propagated to the channel commitments. --- eclair-core/src/main/resources/reference.conf | 3 + .../main/scala/fr/acinq/eclair/Features.scala | 7 + .../eclair/blockchain/fee/FeeEstimator.scala | 4 +- .../eclair/channel/ChannelFeatures.scala | 36 +++-- .../scala/fr/acinq/eclair/FeaturesSpec.scala | 15 ++- .../blockchain/fee/FeeEstimatorSpec.scala | 20 +-- ...lTypesSpec.scala => ChannelDataSpec.scala} | 81 +----------- .../eclair/channel/ChannelFeaturesSpec.scala | 124 ++++++++++++++++++ .../ChannelStateTestsHelperMethods.scala | 6 +- .../a/WaitForAcceptChannelStateSpec.scala | 17 ++- .../a/WaitForOpenChannelStateSpec.scala | 13 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 28 +++- .../eclair/payment/PaymentRequestSpec.scala | 2 +- .../internal/channel/ChannelCodecsSpec.scala | 4 +- .../protocol/LightningMessageCodecsSpec.scala | 5 +- .../acinq/eclair/api/handlers/Channel.scala | 3 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 11 ++ 17 files changed, 259 insertions(+), 120 deletions(-) rename eclair-core/src/test/scala/fr/acinq/eclair/channel/{ChannelTypesSpec.scala => ChannelDataSpec.scala} (82%) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8958e3320a..bef7660ed6 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -49,7 +49,10 @@ eclair { payment_secret = mandatory basic_mpp = optional option_support_large_channel = optional + // NB: option_anchors_zero_fee_htlc_tx should always be preferred to option_anchor_outputs (it's safer). + // Do not enable option_anchor_outputs unless you really know what you're doing. option_anchor_outputs = disabled + option_anchors_zero_fee_htlc_tx = disabled option_shutdown_anysegwit = optional trampoline_payment = disabled keysend = disabled diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index a3247593ab..956855e1b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -193,6 +193,11 @@ object Features { val mandatory = 20 } + case object AnchorOutputsZeroFeeHtlcTxs extends Feature { + val rfcName = "option_anchors_zero_fee_htlc_tx" + val mandatory = 22 + } + case object ShutdownAnySegwit extends Feature { val rfcName = "option_shutdown_anysegwit" val mandatory = 26 @@ -224,6 +229,7 @@ object Features { TrampolinePayment, StaticRemoteKey, AnchorOutputs, + AnchorOutputsZeroFeeHtlcTxs, ShutdownAnySegwit, KeySend ) @@ -236,6 +242,7 @@ object Features { // PaymentSecret -> (VariableLengthOnion :: Nil), BasicMultiPartPayment -> (PaymentSecret :: Nil), AnchorOutputs -> (StaticRemoteKey :: Nil), + AnchorOutputsZeroFeeHtlcTxs -> (StaticRemoteKey :: Nil), TrampolinePayment -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: Nil) ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala index a75b8404bf..df4e822d51 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala @@ -45,6 +45,8 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate case ChannelTypes.AnchorOutputs => proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate + case ChannelTypes.AnchorOutputsZeroFeeHtlcTxs => + proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate } } } @@ -71,7 +73,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget) case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget) } - if (channelType == ChannelTypes.AnchorOutputs) { + if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) { networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate) } else { networkFeerate diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index bb4ca16c08..b9cd2c5b59 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -16,7 +16,6 @@ package fr.acinq.eclair.channel -import fr.acinq.eclair.Features.{AnchorOutputs, OptionUpfrontShutdownScript, StaticRemoteKey, Wumbo} import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat} import fr.acinq.eclair.{Feature, FeatureSupport, Features} @@ -31,19 +30,12 @@ import fr.acinq.eclair.{Feature, FeatureSupport, Features} */ case class ChannelFeatures(activated: Set[Feature]) { - /** Format of the channel transactions. */ - val commitmentFormat: CommitmentFormat = { - if (hasFeature(AnchorOutputs)) { - AnchorOutputsCommitmentFormat - } else { - DefaultCommitmentFormat - } - } - val channelType: SupportedChannelType = { - if (hasFeature(AnchorOutputs)) { + if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTxs)) { + ChannelTypes.AnchorOutputsZeroFeeHtlcTxs + } else if (hasFeature(Features.AnchorOutputs)) { ChannelTypes.AnchorOutputs - } else if (hasFeature(StaticRemoteKey)) { + } else if (hasFeature(Features.StaticRemoteKey)) { ChannelTypes.StaticRemoteKey } else { ChannelTypes.Standard @@ -51,6 +43,7 @@ case class ChannelFeatures(activated: Set[Feature]) { } val paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet + val commitmentFormat: CommitmentFormat = channelType.commitmentFormat def hasFeature(feature: Feature): Boolean = activated.contains(feature) @@ -66,7 +59,7 @@ object ChannelFeatures { def apply(channelType: ChannelType, localFeatures: Features, remoteFeatures: Features): ChannelFeatures = { // NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation, // such as option_dataloss_protect or option_shutdown_anysegwit. - val availableFeatures: Seq[Feature] = Seq(Wumbo, OptionUpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) + val availableFeatures: Seq[Feature] = Seq(Features.Wumbo, Features.OptionUpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) val allFeatures = channelType.features.toSeq ++ availableFeatures ChannelFeatures(allFeatures: _*) } @@ -82,6 +75,9 @@ sealed trait ChannelType { sealed trait SupportedChannelType extends ChannelType { /** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */ def paysDirectlyToWallet: Boolean + + /** Format of the channel transactions. */ + def commitmentFormat: CommitmentFormat } object ChannelTypes { @@ -90,18 +86,27 @@ object ChannelTypes { case object Standard extends SupportedChannelType { override def features: Set[Feature] = Set.empty override def paysDirectlyToWallet: Boolean = false + override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def toString: String = "standard" } case object StaticRemoteKey extends SupportedChannelType { override def features: Set[Feature] = Set(Features.StaticRemoteKey) override def paysDirectlyToWallet: Boolean = true + override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def toString: String = "static_remotekey" } case object AnchorOutputs extends SupportedChannelType { override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) override def paysDirectlyToWallet: Boolean = false + override def commitmentFormat: CommitmentFormat = AnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs" } + case object AnchorOutputsZeroFeeHtlcTxs extends SupportedChannelType { + override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTxs) + override def paysDirectlyToWallet: Boolean = false + override def commitmentFormat: CommitmentFormat = AnchorOutputsCommitmentFormat + override def toString: String = "anchor_outputs_zero_fee_htlc_tx" + } case class UnsupportedChannelType(featureBits: Features) extends ChannelType { override def features: Set[Feature] = featureBits.activated.keySet override def toString: String = s"0x${featureBits.toByteVector.toHex}" @@ -110,6 +115,7 @@ object ChannelTypes { // NB: Bolt 2: features must exactly match in order to identify a channel type. def fromFeatures(features: Features): ChannelType = features match { + case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTxs -> FeatureSupport.Mandatory) => AnchorOutputsZeroFeeHtlcTxs case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory) => AnchorOutputs case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory) => StaticRemoteKey case f if f == Features.empty => Standard @@ -118,7 +124,9 @@ object ChannelTypes { /** Pick the channel type based on local and remote feature bits. */ def pickChannelType(localFeatures: Features, remoteFeatures: Features): SupportedChannelType = { - if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) { + if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTxs)) { + AnchorOutputsZeroFeeHtlcTxs + } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) { AnchorOutputs } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) { StaticRemoteKey diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 26c496db12..f53df88b96 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -87,7 +87,14 @@ class FeaturesSpec extends AnyFunSuite { bin"001000000010000000000000" -> true, bin"001000000001000000000000" -> true, bin"000100000010000000000000" -> true, - bin"000100000001000000000000" -> true + bin"000100000001000000000000" -> true, + // option_anchors_zero_fee_htlc_tx depends on option_static_remotekey + bin"100000000000000000000000" -> false, + bin"010000000000000000000000" -> false, + bin"100000000010000000000000" -> true, + bin"100000000001000000000000" -> true, + bin"010000000010000000000000" -> true, + bin"010000000001000000000000" -> true, ) for ((testCase, valid) <- testCases) { @@ -191,10 +198,10 @@ class FeaturesSpec extends AnyFunSuite { compatible = false ), // nonreg testing of future features (needs to be updated with every new supported mandatory bit) - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(22))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(23))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true) + TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), + TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), + TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), ) for (testCase <- testCases) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala index eabb9399b1..079edf3335 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala @@ -48,7 +48,6 @@ class FeeEstimatorSpec extends AnyFunSuite { test("get commitment feerate (anchor outputs)") { val feeEstimator = new TestFeeEstimator() - val channelType = ChannelTypes.AnchorOutputs val defaultNodeId = randomKey().publicKey val defaultMaxCommitFeerate = FeeratePerKw(2500 sat) val overrideNodeId = randomKey().publicKey @@ -56,18 +55,23 @@ class FeeEstimatorSpec extends AnyFunSuite { val feeConf = OnChainFeeConf(FeeTargets(1, 2, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, FeerateTolerance(0.5, 2.0, defaultMaxCommitFeerate), Map(overrideNodeId -> FeerateTolerance(0.5, 2.0, overrideMaxCommitFeerate))) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2)) - assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, None) === defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, None) === defaultMaxCommitFeerate / 2) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 2)) - assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, None) === defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(overrideNodeId, channelType, 100000 sat, None) === overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, None) === defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, None) === overrideMaxCommitFeerate) val currentFeerates1 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2)) - assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2) val currentFeerates2 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 1.5)) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2)) - assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate) } test("fee difference too high") { @@ -91,7 +95,6 @@ class FeeEstimatorSpec extends AnyFunSuite { test("fee difference too high (anchor outputs)") { val tolerance = FeerateTolerance(ratioLow = 0.5, ratioHigh = 4.0, anchorOutputMaxCommitFeerate = FeeratePerKw(2500 sat)) - val channelType = ChannelTypes.AnchorOutputs val testCases = Seq( (FeeratePerKw(500 sat), FeeratePerKw(500 sat), false), (FeeratePerKw(500 sat), FeeratePerKw(2500 sat), false), @@ -106,7 +109,8 @@ class FeeEstimatorSpec extends AnyFunSuite { (FeeratePerKw(1000 sat), FeeratePerKw(499 sat), true), ) testCases.foreach { case (networkFeerate, proposedFeerate, expected) => - assert(tolerance.isFeeDiffTooHigh(channelType, networkFeerate, proposedFeerate) === expected) + assert(tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputs, networkFeerate, proposedFeerate) === expected) + assert(tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, networkFeerate, proposedFeerate) === expected) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala similarity index 82% rename from eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala rename to eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala index 42b107899e..48983db99f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala @@ -18,96 +18,19 @@ package fr.acinq.eclair.channel import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn, TxOut} -import fr.acinq.eclair.FeatureSupport._ -import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods -import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UpdateAddHtlc} -import fr.acinq.eclair.{Features, MilliSatoshiLong, TestKitBaseClass} +import fr.acinq.eclair.{MilliSatoshiLong, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector -class ChannelTypesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { +class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging - test("channel features determines commitment format") { - val standardChannel = ChannelFeatures() - val staticRemoteKeyChannel = ChannelFeatures(Features.StaticRemoteKey) - val anchorOutputsChannel = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs) - assert(!standardChannel.hasFeature(Features.StaticRemoteKey)) - assert(!standardChannel.hasFeature(Features.AnchorOutputs)) - assert(standardChannel.commitmentFormat === Transactions.DefaultCommitmentFormat) - assert(!standardChannel.paysDirectlyToWallet) - - assert(staticRemoteKeyChannel.hasFeature(Features.StaticRemoteKey)) - assert(!staticRemoteKeyChannel.hasFeature(Features.AnchorOutputs)) - assert(staticRemoteKeyChannel.commitmentFormat === Transactions.DefaultCommitmentFormat) - assert(staticRemoteKeyChannel.paysDirectlyToWallet) - - assert(anchorOutputsChannel.hasFeature(Features.StaticRemoteKey)) - assert(anchorOutputsChannel.hasFeature(Features.AnchorOutputs)) - assert(anchorOutputsChannel.commitmentFormat === Transactions.AnchorOutputsCommitmentFormat) - assert(!anchorOutputsChannel.paysDirectlyToWallet) - } - - test("pick channel type based on local and remote features") { - case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelType: ChannelType) - val testCases = Seq( - TestCase(Features.empty, Features.empty, ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelTypes.Standard), - TestCase(Features.empty, Features(StaticRemoteKey -> Optional), ChannelTypes.Standard), - TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.AnchorOutputs) - ) - - for (testCase <- testCases) { - assert(ChannelTypes.pickChannelType(testCase.localFeatures, testCase.remoteFeatures) === testCase.expectedChannelType) - } - } - - test("create channel type from features") { - val validChannelTypes = Seq( - Features.empty -> ChannelTypes.Standard, - Features(StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey, - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs, - ) - for ((features, expected) <- validChannelTypes) { - assert(ChannelTypes.fromFeatures(features) === expected) - } - - val invalidChannelTypes = Seq( - Features(Wumbo -> Optional), - Features(StaticRemoteKey -> Optional), - Features(StaticRemoteKey -> Mandatory, Wumbo -> Optional), - Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), - Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory), - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), - ) - for (features <- invalidChannelTypes) { - assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features)) - } - } - - test("enrich channel type with other permanent channel features") { - assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty).activated.isEmpty) - assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).activated === Set(Wumbo)) - assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional)).activated === Set(Wumbo)) - assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty).activated === Set(StaticRemoteKey)) - assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, Wumbo)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, AnchorOutputs)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).activated === Set(StaticRemoteKey, AnchorOutputs, Wumbo)) - } - case class HtlcWithPreimage(preimage: ByteVector32, htlc: UpdateAddHtlc) case class Fixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], alicePendingHtlc: HtlcWithPreimage, bob: TestFSMRef[ChannelState, ChannelData, Channel], bobPendingHtlc: HtlcWithPreimage, probe: TestProbe) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala new file mode 100644 index 0000000000..616f1c7ac8 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -0,0 +1,124 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel + +import fr.acinq.eclair.FeatureSupport._ +import fr.acinq.eclair.Features._ +import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods +import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.{Features, TestKitBaseClass} +import org.scalatest.funsuite.AnyFunSuiteLike + +class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { + + test("channel features determines commitment format") { + val standardChannel = ChannelFeatures() + val staticRemoteKeyChannel = ChannelFeatures(Features.StaticRemoteKey) + val anchorOutputsChannel = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs) + val anchorOutputsZeroFeeHtlcsChannel = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTxs) + + assert(!standardChannel.hasFeature(Features.StaticRemoteKey)) + assert(!standardChannel.hasFeature(Features.AnchorOutputs)) + assert(standardChannel.commitmentFormat === Transactions.DefaultCommitmentFormat) + assert(!standardChannel.paysDirectlyToWallet) + + assert(staticRemoteKeyChannel.hasFeature(Features.StaticRemoteKey)) + assert(!staticRemoteKeyChannel.hasFeature(Features.AnchorOutputs)) + assert(staticRemoteKeyChannel.commitmentFormat === Transactions.DefaultCommitmentFormat) + assert(staticRemoteKeyChannel.paysDirectlyToWallet) + + assert(anchorOutputsChannel.hasFeature(Features.StaticRemoteKey)) + assert(anchorOutputsChannel.hasFeature(Features.AnchorOutputs)) + assert(!anchorOutputsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTxs)) + assert(anchorOutputsChannel.commitmentFormat === Transactions.AnchorOutputsCommitmentFormat) + assert(!anchorOutputsChannel.paysDirectlyToWallet) + + assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.StaticRemoteKey)) + assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTxs)) + assert(!anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputs)) + assert(anchorOutputsZeroFeeHtlcsChannel.commitmentFormat === Transactions.AnchorOutputsCommitmentFormat) + assert(!anchorOutputsZeroFeeHtlcsChannel.paysDirectlyToWallet) + } + + test("pick channel type based on local and remote features") { + case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelType: ChannelType) + val testCases = Seq( + TestCase(Features.empty, Features.empty, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelTypes.Standard), + TestCase(Features.empty, Features(StaticRemoteKey -> Optional), ChannelTypes.Standard), + TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional), ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTxs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTxs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTxs), + ) + + for (testCase <- testCases) { + assert(ChannelTypes.pickChannelType(testCase.localFeatures, testCase.remoteFeatures) === testCase.expectedChannelType) + } + } + + test("create channel type from features") { + val validChannelTypes = Seq( + Features.empty -> ChannelTypes.Standard, + Features(StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey, + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs, + Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Mandatory) -> ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, + ) + for ((features, expected) <- validChannelTypes) { + assert(ChannelTypes.fromFeatures(features) === expected) + } + + val invalidChannelTypes = Seq( + Features(Wumbo -> Optional), + Features(StaticRemoteKey -> Optional), + Features(StaticRemoteKey -> Mandatory, Wumbo -> Optional), + Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), + Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory), + Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional), + Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Mandatory), + Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Optional), + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Mandatory), + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Mandatory), + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), + ) + for (features <- invalidChannelTypes) { + assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features)) + } + } + + test("enrich channel type with other permanent channel features") { + assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty).activated.isEmpty) + assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).activated === Set(Wumbo)) + assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional)).activated === Set(Wumbo)) + assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty).activated === Set(StaticRemoteKey)) + assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, Wumbo)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, AnchorOutputs)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).activated === Set(StaticRemoteKey, AnchorOutputs, Wumbo)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, Features.empty, Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTxs)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).activated === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTxs, Wumbo)) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 0b5ee131b8..29d482bbd3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -62,6 +62,8 @@ object ChannelStateTestsTags { val StaticRemoteKey = "static_remotekey" /** If set, channels will use option_anchor_outputs. */ val AnchorOutputs = "anchor_outputs" + /** If set, channels will use option_anchors_zero_fee_htlc_tx. */ + val AnchorOutputsZeroFeeHtlcTxs = "anchor_outputs_zero_fee_htlc_tx" /** If set, channels will use option_shutdown_anysegwit. */ val ShutdownAnySegwit = "shutdown_anysegwit" /** If set, channels will be public (otherwise we don't announce them by default). */ @@ -121,12 +123,14 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTxs, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.OptionUpfrontShutdownScript, FeatureSupport.Optional)) val bobInitFeatures = Bob.nodeParams.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTxs, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.OptionUpfrontShutdownScript, FeatureSupport.Optional)) @@ -152,7 +156,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags) val channelFlags = if (tags.contains(ChannelStateTestsTags.ChannelsPublic)) ChannelFlags.AnnounceChannel else ChannelFlags.Empty - val initialFeeratePerKw = if (tags.contains(ChannelStateTestsTags.AnchorOutputs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val initialFeeratePerKw = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val (fundingSatoshis, pushMsat) = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) { (TestConstants.fundingSatoshis, 0.msat) } else { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 836b2dff51..7b29c2a936 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -62,7 +62,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { @@ -95,6 +95,15 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) } + test("recv AcceptChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs)) + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) + } + test("recv AcceptChannel (channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] @@ -106,7 +115,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) } - test("recv AcceptChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag("standard-channel-type")) { f => + test("recv AcceptChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // Alice asked for a standard channel whereas they both support anchor outputs. @@ -116,14 +125,14 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.Standard) } - test("recv AcceptChannel (non-default channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag("standard-channel-type")) { f => + test("recv AcceptChannel (non-default channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt === Some(ChannelTypes.Standard)) // Alice asked for a standard channel whereas they both support anchor outputs. Bob doesn't support explicit channel // type negotiation so Alice needs to abort because the channel types won't match. bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) - alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard")) + alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs_zero_fee_htlc_tx, expected channel_type=standard")) awaitCond(alice.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 6fc21b5183..285115d8ed 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -51,7 +51,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { @@ -83,7 +83,16 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.AnchorOutputs) } - test("recv OpenChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag("standard-channel-type")) { f => + test("recv OpenChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs)) + alice2bob.forward(bob) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) + } + + test("recv OpenChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] assert(open.channelType_opt === Some(ChannelTypes.Standard)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 488ecb0c64..7c14001513 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -23,7 +23,7 @@ import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, Btc, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} -import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey, Wumbo} +import fr.acinq.eclair.Features.{AnchorOutputs, AnchorOutputsZeroFeeHtlcTxs, StaticRemoteKey, Wumbo} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw @@ -68,6 +68,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle .modify(_.features).setToIf(test.tags.contains("static_remotekey"))(Features(StaticRemoteKey -> Optional)) .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Wumbo -> Optional)) .modify(_.features).setToIf(test.tags.contains("anchor_outputs"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional)) + .modify(_.features).setToIf(test.tags.contains("anchor_outputs_zero_fee_htlc_tx"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional)) .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) @@ -317,6 +318,13 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard")) } + // They only support anchor outputs with zero fee htlc txs and we don't. + { + val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs)) + val open = protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0, openTlv) + peerConnection.send(peer, open) + peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs_zero_fee_htlc_tx, expected channel_type=standard")) + } // They want to use a channel type that doesn't exist in the spec. { val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(AnchorOutputs -> Optional)))) @@ -384,6 +392,24 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } + test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)", Tag("anchor_outputs_zero_fee_htlc_tx")) { f => + import f._ + + val probe = TestProbe() + connect(remoteNodeId, peer, peerConnection, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional))) + assert(peer.stateData.channels.isEmpty) + + // We ensure the current network feerate is higher than the default anchor output feerate. + val feeEstimator = nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] + feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) + val init = channel.expectMsgType[INPUT_INIT_FUNDER] + assert(init.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) + assert(init.fundingAmount === 15000.sat) + assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw) + assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + } + test("use correct final script if option_static_remotekey is negotiated", Tag("static_remotekey")) { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala index 94c957a8c6..f970ae0ac0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala @@ -396,8 +396,8 @@ class PaymentRequestSpec extends AnyFunSuite { PaymentRequestFeatures(bin" 0000011000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), PaymentRequestFeatures(bin" 0000110000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), PaymentRequestFeatures(bin" 0000100000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + PaymentRequestFeatures(bin" 0010000000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), // those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit) - PaymentRequestFeatures(bin" 0010000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), PaymentRequestFeatures(bin" 000001000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), PaymentRequestFeatures(bin" 000100000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), PaymentRequestFeatures(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index f9cf742971..4c684ec594 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -125,14 +125,13 @@ class ChannelCodecsSpec extends AnyFunSuite { test("backward compatibility older codecs (integrity)") { // this test makes sure that we actually produce the same objects than previous versions of eclair - val refs = Map( hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"feeratePerKw":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"feeratePerKw":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":1560862173,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":1561369173,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"gossip_queries":"optional","option_shutdown_anysegwit":"optional","payment_secret":"optional","option_data_loss_protect":"optional","option_static_remotekey":"optional","basic_mpp":"optional","gossip_queries_ex":"optional","option_support_large_channel":"optional","var_onion_optin":"mandatory"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"gossip_queries":"optional","basic_mpp":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","option_static_remotekey":"mandatory","option_upfront_shutdown_script":"optional","option_support_large_channel":"optional","var_onion_optin":"optional"},"unknown":[23,31]}},"channelFlags":1,"localCommit":{"index":4,"spec":{"htlcs":[],"feeratePerKw":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"feeratePerKw":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":1625746196,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"gossip_queries":"optional","option_shutdown_anysegwit":"optional","payment_secret":"optional","option_data_loss_protect":"optional","option_static_remotekey":"optional","basic_mpp":"optional","gossip_queries_ex":"optional","option_support_large_channel":"optional","var_onion_optin":"mandatory"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"gossip_queries":"optional","basic_mpp":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","option_static_remotekey":"mandatory","option_anchors_zero_fee_htlc_tx":"optional","option_upfront_shutdown_script":"optional","option_support_large_channel":"optional","var_onion_optin":"optional"},"unknown":[31]}},"channelFlags":1,"localCommit":{"index":4,"spec":{"htlcs":[],"feeratePerKw":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"feeratePerKw":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":1625746196,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" ) refs.foreach { case (oldbin, refjson) => @@ -145,7 +144,6 @@ class ChannelCodecsSpec extends AnyFunSuite { // finally we check that the actual data is the same as before (we just remove the new json field) val oldjson = Serialization.write(oldnormal)(JsonSerializers.formats) val newjson = Serialization.write(newnormal)(JsonSerializers.formats) - assert(oldjson === refjson) assert(newjson === refjson) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index a2becaf3ac..946b0a6ef7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -168,10 +168,13 @@ class LightningMessageCodecsSpec extends AnyFunSuite { defaultEncoded ++ hex"0002 1234 0303010203" -> defaultOpen.copy(tlvStream = TlvStream(Seq(ChannelTlv.UpfrontShutdownScriptTlv(hex"1234")), Seq(GenericTlv(UInt64(3), hex"010203")))), // empty upfront_shutdown_script + default channel type defaultEncoded ++ hex"0000" ++ hex"0100" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard))), + // empty upfront_shutdown_script + unsupported channel type + defaultEncoded ++ hex"0000" ++ hex"0103501000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTxs -> FeatureSupport.Mandatory))))), // empty upfront_shutdown_script + channel type defaultEncoded ++ hex"0000" ++ hex"01021000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey))), // non-empty upfront_shutdown_script + channel type - defaultEncoded ++ hex"0004 01abcdef" ++ hex"0103101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))) + defaultEncoded ++ hex"0004 01abcdef" ++ hex"0103101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))), + defaultEncoded ++ hex"0002 abcd" ++ hex"0103401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"abcd"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs))), ) for ((encoded, expected) <- testCases) { diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index d243fddc55..f18777be32 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -39,11 +39,12 @@ trait Channel { case Some(str) if str == ChannelTypes.Standard.toString => (true, Some(ChannelTypes.Standard)) case Some(str) if str == ChannelTypes.StaticRemoteKey.toString => (true, Some(ChannelTypes.StaticRemoteKey)) case Some(str) if str == ChannelTypes.AnchorOutputs.toString => (true, Some(ChannelTypes.AnchorOutputs)) + case Some(str) if str == ChannelTypes.AnchorOutputsZeroFeeHtlcTxs.toString => (true, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs)) case Some(_) => (false, None) case None => (true, None) } if (!channelTypeOk) { - reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be ${ChannelTypes.Standard.toString}, ${ChannelTypes.StaticRemoteKey.toString} or ${ChannelTypes.AnchorOutputs.toString}")) + reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be ${ChannelTypes.Standard.toString}, ${ChannelTypes.StaticRemoteKey.toString}, ${ChannelTypes.AnchorOutputs.toString} or ${ChannelTypes.AnchorOutputsZeroFeeHtlcTxs.toString}")) } else { complete { eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, channelFlags, openTimeout_opt) diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index be25b598ee..d32a486c56 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -341,6 +341,17 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"") eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputs), None, None, None)(any[Timeout]).wasCalled(once) } + + Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "anchor_outputs_zero_fee_htlc_tx").toEntity) ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + addHeader("Content-Type", "application/json") ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"") + eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs), None, None, None)(any[Timeout]).wasCalled(once) + } } test("'close' method should accept shortChannelIds") { From 371c183d0e8443511682b418fb404925ef8a0ce9 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 2 Sep 2021 09:46:31 +0200 Subject: [PATCH 2/3] Set htlc tx fee to zero When the channel type is anchor outputs with zero fee htlc txs, we set the fees for the htlc txs to 0. An important side-effect is that it changes the trimmed to dust calculation, and outputs that were previously dust can now be included in the commit tx. --- .../main/scala/fr/acinq/eclair/Features.scala | 6 +- .../eclair/blockchain/fee/FeeEstimator.scala | 9 +- .../fr/acinq/eclair/channel/Channel.scala | 20 +-- .../eclair/channel/ChannelFeatures.scala | 14 +- .../fr/acinq/eclair/channel/Commitments.scala | 46 +++--- .../fr/acinq/eclair/channel/Helpers.scala | 28 ++-- .../publish/ReplaceableTxPublisher.scala | 8 +- .../relay/PostRestartHtlcCleaner.scala | 4 +- .../eclair/transactions/CommitmentSpec.scala | 12 +- .../eclair/transactions/Transactions.scala | 53 +++---- .../blockchain/fee/FeeEstimatorSpec.scala | 12 +- .../eclair/channel/ChannelFeaturesSpec.scala | 32 ++-- .../fr/acinq/eclair/channel/HelpersSpec.scala | 26 ++-- .../publish/ReplaceableTxPublisherSpec.scala | 74 +++++---- .../ChannelStateTestsHelperMethods.scala | 6 +- .../a/WaitForAcceptChannelStateSpec.scala | 6 +- .../a/WaitForOpenChannelStateSpec.scala | 6 +- .../channel/states/e/NormalStateSpec.scala | 142 ++++++++++++++---- .../channel/states/e/OfflineStateSpec.scala | 8 +- .../channel/states/f/ShutdownStateSpec.scala | 8 +- .../channel/states/h/ClosingStateSpec.scala | 58 ++++--- .../integration/ChannelIntegrationSpec.scala | 118 +++++++++++---- .../eclair/integration/IntegrationSpec.scala | 4 + .../integration/PaymentIntegrationSpec.scala | 4 +- .../rustytests/SynchronizationPipe.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 10 +- .../transactions/CommitmentSpecSpec.scala | 17 ++- .../eclair/transactions/TestVectorsSpec.scala | 17 +-- .../transactions/TransactionsSpec.scala | 81 ++++++---- .../internal/channel/ChannelCodecsSpec.scala | 6 +- .../channel/version1/ChannelCodecs1Spec.scala | 2 +- .../protocol/LightningMessageCodecsSpec.scala | 4 +- .../acinq/eclair/api/handlers/Channel.scala | 4 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- 34 files changed, 545 insertions(+), 306 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 956855e1b5..5dba8a4a7e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -193,7 +193,7 @@ object Features { val mandatory = 20 } - case object AnchorOutputsZeroFeeHtlcTxs extends Feature { + case object AnchorOutputsZeroFeeHtlcTx extends Feature { val rfcName = "option_anchors_zero_fee_htlc_tx" val mandatory = 22 } @@ -229,7 +229,7 @@ object Features { TrampolinePayment, StaticRemoteKey, AnchorOutputs, - AnchorOutputsZeroFeeHtlcTxs, + AnchorOutputsZeroFeeHtlcTx, ShutdownAnySegwit, KeySend ) @@ -242,7 +242,7 @@ object Features { // PaymentSecret -> (VariableLengthOnion :: Nil), BasicMultiPartPayment -> (PaymentSecret :: Nil), AnchorOutputs -> (StaticRemoteKey :: Nil), - AnchorOutputsZeroFeeHtlcTxs -> (StaticRemoteKey :: Nil), + AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil), TrampolinePayment -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: Nil) ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala index df4e822d51..a9d49c5fcb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala @@ -19,7 +19,8 @@ package fr.acinq.eclair.blockchain.fee import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.blockchain.CurrentFeerates -import fr.acinq.eclair.channel.{ChannelType, ChannelTypes, SupportedChannelType} +import fr.acinq.eclair.channel.{ChannelTypes, SupportedChannelType} +import fr.acinq.eclair.transactions.Transactions.AnchorOutputsCommitmentFormat trait FeeEstimator { // @formatter:off @@ -45,7 +46,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate case ChannelTypes.AnchorOutputs => proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate - case ChannelTypes.AnchorOutputsZeroFeeHtlcTxs => + case ChannelTypes.AnchorOutputsZeroFeeHtlcTx => proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate } } @@ -68,12 +69,12 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl * @param channelType channel type * @param currentFeerates_opt if provided, will be used to compute the most up-to-date network fee, otherwise we rely on the fee estimator */ - def getCommitmentFeerate(remoteNodeId: PublicKey, channelType: ChannelType, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = { + def getCommitmentFeerate(remoteNodeId: PublicKey, channelType: SupportedChannelType, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = { val networkFeerate = currentFeerates_opt match { case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget) case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget) } - if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) { + if (channelType.commitmentFormat == AnchorOutputsCommitmentFormat) { networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate) } else { networkFeerate diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 464f01cb6a..f70858af67 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -789,8 +789,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val nextCommitNumber = nextRemoteCommit.index // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter - val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.commitmentFormat) ++ - Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.commitmentFormat) + val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.channelType) ++ + Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.channelType) trimmedHtlcs.map(_.add).foreach { htlc => log.info(s"adding paymentHash=${htlc.paymentHash} cltvExpiry=${htlc.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, htlc.paymentHash, htlc.cltvExpiry) @@ -1147,8 +1147,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val nextCommitNumber = nextRemoteCommit.index // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter - val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.commitmentFormat) ++ - Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.commitmentFormat) + val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.channelType) ++ + Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.channelType) trimmedHtlcs.map(_.add).foreach { htlc => log.info(s"adding paymentHash=${htlc.paymentHash} cltvExpiry=${htlc.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, htlc.paymentHash, htlc.cltvExpiry) @@ -1488,8 +1488,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold val timedOutHtlcs = Closing.isClosingTypeAlreadyKnown(d1) match { - case Some(c: Closing.LocalClose) => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx) - case Some(c: Closing.RemoteClose) => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx) + case Some(c: Closing.LocalClose) => Closing.timedOutHtlcs(d.commitments.channelType, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx) + case Some(c: Closing.RemoteClose) => Closing.timedOutHtlcs(d.commitments.channelType, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx) case _ => Set.empty[UpdateAddHtlc] // we lose htlc outputs in dataloss protection scenarii (future remote commit) } timedOutHtlcs.foreach { add => @@ -1733,7 +1733,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we send it (if needed) when reconnected. val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty if (d.commitments.localParams.isFunder && !shutdownInProgress) { - val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw + val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, None) if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) { self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) @@ -2031,7 +2031,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId private def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = { val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c)) - val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw + val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate val shouldUpdateFee = d.commitments.localParams.isFunder && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw) val shouldClose = !d.commitments.localParams.isFunder && nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) && @@ -2040,7 +2040,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) stay() } else if (shouldClose) { - handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c)) + handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate), d, Some(c)) } else { stay() } @@ -2055,7 +2055,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId */ private def handleCurrentFeerateDisconnected(c: CurrentFeerates, d: HasCommitments) = { val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c)) - val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw + val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate // if the network fees are too high we risk to not be able to confirm our current commitment val shouldClose = networkFeeratePerKw > currentFeeratePerKw && nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) && diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index b9cd2c5b59..373d9ee7b2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -31,8 +31,8 @@ import fr.acinq.eclair.{Feature, FeatureSupport, Features} case class ChannelFeatures(activated: Set[Feature]) { val channelType: SupportedChannelType = { - if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTxs)) { - ChannelTypes.AnchorOutputsZeroFeeHtlcTxs + if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { + ChannelTypes.AnchorOutputsZeroFeeHtlcTx } else if (hasFeature(Features.AnchorOutputs)) { ChannelTypes.AnchorOutputs } else if (hasFeature(Features.StaticRemoteKey)) { @@ -101,8 +101,8 @@ object ChannelTypes { override def commitmentFormat: CommitmentFormat = AnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs" } - case object AnchorOutputsZeroFeeHtlcTxs extends SupportedChannelType { - override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTxs) + case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType { + override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = AnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs_zero_fee_htlc_tx" @@ -115,7 +115,7 @@ object ChannelTypes { // NB: Bolt 2: features must exactly match in order to identify a channel type. def fromFeatures(features: Features): ChannelType = features match { - case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTxs -> FeatureSupport.Mandatory) => AnchorOutputsZeroFeeHtlcTxs + case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Mandatory) => AnchorOutputsZeroFeeHtlcTx case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory) => AnchorOutputs case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory) => StaticRemoteKey case f if f == Features.empty => Standard @@ -124,8 +124,8 @@ object ChannelTypes { /** Pick the channel type based on local and remote feature bits. */ def pickChannelType(localFeatures: Features, remoteFeatures: Features): SupportedChannelType = { - if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTxs)) { - AnchorOutputsZeroFeeHtlcTxs + if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) { + AnchorOutputsZeroFeeHtlcTx } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) { AnchorOutputs } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index f875798a4b..dc79f63076 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -242,18 +242,18 @@ case class Commitments(channelId: ByteVector32, val balanceNoFees = (reduced.toRemote - remoteParams.channelReserve).max(0 msat) if (localParams.isFunder) { // The funder always pays the on-chain fees, so we must subtract that from the amount we can send. - val commitFees = commitTxTotalCostMsat(remoteParams.dustLimit, reduced, commitmentFormat) + val commitFees = commitTxTotalCostMsat(remoteParams.dustLimit, reduced, channelType) // the funder needs to keep a "funder fee buffer" (see explanation above) - val funderFeeBuffer = commitTxTotalCostMsat(remoteParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat) + val funderFeeBuffer = commitTxTotalCostMsat(remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), channelType) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve = commitFees.max(funderFeeBuffer) - if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced, commitmentFormat)) { + if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced, channelType)) { // htlc will be trimmed (balanceNoFees - amountToReserve).max(0 msat) } else { // htlc will have an output in the commitment tx, so there will be additional fees. - val commitFees1 = commitFees + htlcOutputFee(reduced.feeratePerKw, commitmentFormat) + val commitFees1 = commitFees + htlcOutputFee(reduced.commitTxFeerate, commitmentFormat) // we take the additional fees for that htlc output into account in the fee buffer at a x2 feerate increase - val funderFeeBuffer1 = funderFeeBuffer + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat) + val funderFeeBuffer1 = funderFeeBuffer + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve1 = commitFees1.max(funderFeeBuffer1) (balanceNoFees - amountToReserve1).max(0 msat) } @@ -271,18 +271,18 @@ case class Commitments(channelId: ByteVector32, balanceNoFees } else { // The funder always pays the on-chain fees, so we must subtract that from the amount we can receive. - val commitFees = commitTxTotalCostMsat(localParams.dustLimit, reduced, commitmentFormat) + val commitFees = commitTxTotalCostMsat(localParams.dustLimit, reduced, channelType) // we expected the funder to keep a "funder fee buffer" (see explanation above) - val funderFeeBuffer = commitTxTotalCostMsat(localParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat) + val funderFeeBuffer = commitTxTotalCostMsat(localParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), channelType) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve = commitFees.max(funderFeeBuffer) - if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localParams.dustLimit, reduced, commitmentFormat)) { + if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localParams.dustLimit, reduced, channelType)) { // htlc will be trimmed (balanceNoFees - amountToReserve).max(0 msat) } else { // htlc will have an output in the commitment tx, so there will be additional fees. - val commitFees1 = commitFees + htlcOutputFee(reduced.feeratePerKw, commitmentFormat) + val commitFees1 = commitFees + htlcOutputFee(reduced.commitTxFeerate, commitmentFormat) // we take the additional fees for that htlc output into account in the fee buffer at a x2 feerate increase - val funderFeeBuffer1 = funderFeeBuffer + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat) + val funderFeeBuffer1 = funderFeeBuffer + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve1 = commitFees1.max(funderFeeBuffer1) (balanceNoFees - amountToReserve1).max(0 msat) } @@ -340,7 +340,7 @@ object Commitments { // we need to verify that we're not disagreeing on feerates anymore before offering new HTLCs // NB: there may be a pending update_fee that hasn't been applied yet that needs to be taken into account val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelType, commitments.capacity, None) - val remoteFeeratePerKw = commitments.localCommit.spec.feeratePerKw +: commitments.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw } + val remoteFeeratePerKw = commitments.localCommit.spec.commitTxFeerate +: commitments.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw } remoteFeeratePerKw.find(feerate => feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelType, localFeeratePerKw, feerate)) match { case Some(feerate) => return Left(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = feerate)) case None => @@ -357,10 +357,10 @@ object Commitments { val outgoingHtlcs = reduced.htlcs.collect(incoming) // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment - val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) + val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.channelType) // the funder needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid // getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728). - val funderFeeBuffer = commitTxTotalCostMsat(commitments1.remoteParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitments.commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitments.commitmentFormat) + val funderFeeBuffer = commitTxTotalCostMsat(commitments1.remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitments.channelType) + htlcOutputFee(reduced.commitTxFeerate * 2, commitments.commitmentFormat) // NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold) // which may result in a lower commit tx fee; this is why we take the max of the two. val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) @@ -404,7 +404,7 @@ object Commitments { // we need to verify that we're not disagreeing on feerates anymore before accepting new HTLCs // NB: there may be a pending update_fee that hasn't been applied yet that needs to be taken into account val localFeeratePerKw = feeConf.getCommitmentFeerate(commitments.remoteNodeId, commitments.channelType, commitments.capacity, None) - val remoteFeeratePerKw = commitments.localCommit.spec.feeratePerKw +: commitments.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw } + val remoteFeeratePerKw = commitments.localCommit.spec.commitTxFeerate +: commitments.remoteChanges.all.collect { case f: UpdateFee => f.feeratePerKw } remoteFeeratePerKw.find(feerate => feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelType, localFeeratePerKw, feerate)) match { case Some(feerate) => return Left(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = feerate)) case None => @@ -416,7 +416,7 @@ object Commitments { val incomingHtlcs = reduced.htlcs.collect(incoming) // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment - val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) + val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.channelType) // NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote funder that doesn't have this mitigation in place // We could enforce it once we're confident a large portion of the network implements it. val missingForSender = reduced.toRemote - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees) @@ -535,7 +535,7 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is funder remote doesn't pay the fees - val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) + val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.channelType) val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteParams.channelReserve - fees if (missing < 0.sat) { Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees)) @@ -568,7 +568,7 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee - val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) + val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.channelType) val missing = reduced.toRemote.truncateToSatoshi - commitments1.localParams.channelReserve - fees if (missing < 0.sat) { Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees)) @@ -611,7 +611,7 @@ object Commitments { val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint, TxOwner.Remote, commitmentFormat)) // NB: IN/OUT htlcs are inverted because this is the remote commit - log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.collect(outgoing).map(_.id).mkString(","), spec.htlcs.collect(incoming).map(_.id).mkString(","), remoteCommitTx.tx) + log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.collect(outgoing).map(_.id).mkString(","), spec.htlcs.collect(incoming).map(_.id).mkString(","), remoteCommitTx.tx) Metrics.recordHtlcsInFlight(spec, remoteCommit.spec) val commitSig = CommitSig( @@ -650,7 +650,7 @@ object Commitments { val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index + 1) val (localCommitTx, htlcTxs) = makeLocalTxs(keyManager, channelConfig, channelFeatures, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) - log.info(s"built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${localCommitTx.tx.txid} tx={}", spec.htlcs.collect(incoming).map(_.id).mkString(","), spec.htlcs.collect(outgoing).map(_.id).mkString(","), localCommitTx.tx) + log.info(s"built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommitTx.tx.txid} tx={}", spec.htlcs.collect(incoming).map(_.id).mkString(","), spec.htlcs.collect(outgoing).map(_.id).mkString(","), localCommitTx.tx) if (!Transactions.checkSig(localCommitTx, commit.signature, remoteParams.fundingPubKey, TxOwner.Remote, commitmentFormat)) { return Left(InvalidCommitmentSignature(commitments.channelId, localCommitTx.tx)) @@ -754,9 +754,9 @@ object Commitments { val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelFeatures.commitmentFormat) + val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelFeatures.channelType) val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isFunder, outputs) - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feeratePerKw, outputs, channelFeatures.commitmentFormat) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.channelType), outputs, channelFeatures.commitmentFormat) (commitTx, htlcTxs) } @@ -781,9 +781,9 @@ object Commitments { val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, spec, channelFeatures.commitmentFormat) + val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, spec, channelFeatures.channelType) val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentBasepoint, !localParams.isFunder, outputs) - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feeratePerKw, outputs, channelFeatures.commitmentFormat) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.channelType), outputs, channelFeatures.commitmentFormat) (commitTx, htlcTxs) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index e4f172a4cb..619d4439ad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -289,13 +289,13 @@ object Helpers { val toLocalMsat = if (localParams.isFunder) fundingAmount.toMilliSatoshi - pushMsat else pushMsat val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingAmount.toMilliSatoshi - pushMsat - val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocal = toLocalMsat, toRemote = toRemoteMsat) - val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocal = toRemoteMsat, toRemote = toLocalMsat) + val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], initialFeeratePerKw, toLocal = toLocalMsat, toRemote = toRemoteMsat) + val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], initialFeeratePerKw, 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 toRemoteMsat = remoteSpec.toLocal - val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat) + val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.channelType) val missing = toRemoteMsat.truncateToSatoshi - localParams.channelReserve - fees if (missing < Satoshi(0)) { return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.channelReserve, fees = fees)) @@ -472,11 +472,11 @@ object Helpers { def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): ClosingFees = { val requestedFeerate = feeEstimator.getFeeratePerKw(feeTargets.mutualCloseBlockTarget) - val preferredFeerate = if (commitments.channelFeatures.hasFeature(Features.AnchorOutputs)) { + val preferredFeerate = if (commitments.commitmentFormat == AnchorOutputsCommitmentFormat) { requestedFeerate } else { // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" - requestedFeerate.min(commitments.localCommit.spec.feeratePerKw) + requestedFeerate.min(commitments.localCommit.spec.commitTxFeerate) } // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can // always use CPFP to speed up confirmation if necessary. @@ -514,7 +514,7 @@ object Helpers { def checkClosingSignature(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = { import commitments._ val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount - commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.map(_.amount).sum - if (remoteClosingFee > lastCommitFeeSatoshi && !commitments.channelFeatures.hasFeature(Features.AnchorOutputs)) { + if (remoteClosingFee > lastCommitFeeSatoshi && commitments.commitmentFormat != AnchorOutputsCommitmentFormat) { log.error(s"remote proposed a commit fee higher than the last commitment fee: remote closing fee=${remoteClosingFee.toLong} last commit fees=$lastCommitFeeSatoshi") Left(InvalidCloseFee(commitments.channelId, remoteClosingFee)) } else { @@ -658,7 +658,7 @@ object Helpers { val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, remoteCommit.spec, commitments.commitmentFormat) + val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, remoteCommit.spec, commitments.channelType) // we need to use a rather high fee for htlc-claim because we compete with the counterparty val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) @@ -800,7 +800,7 @@ object Helpers { case ct if ct.paysDirectlyToWallet => log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do") None - case ct if ct.hasFeature(Features.AnchorOutputs) => generateTx("remote-main-delayed") { + case ct if ct.commitmentFormat == AnchorOutputsCommitmentFormat => generateTx("remote-main-delayed") { Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) Transactions.addSigs(claimMain, sig) @@ -1003,15 +1003,15 @@ object Helpers { * @param tx a tx that has reached mindepth * @return a set of htlcs that need to be failed upstream */ - def timedOutHtlcs(commitmentFormat: CommitmentFormat, localCommit: LocalCommit, localCommitPublished: LocalCommitPublished, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { - val untrimmedHtlcs = Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec, commitmentFormat).map(_.add) + def timedOutHtlcs(channelType: SupportedChannelType, localCommit: LocalCommit, localCommitPublished: LocalCommitPublished, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { + val untrimmedHtlcs = Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec, channelType).map(_.add) if (tx.txid == localCommit.commitTxAndRemoteSig.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) localCommit.spec.htlcs.collect(outgoing) -- untrimmedHtlcs } else { // maybe this is a timeout tx, in that case we can resolve and fail the corresponding htlc val isMissingHtlcIndex = localCommitPublished.htlcTxs.values.collect { case Some(HtlcTimeoutTx(_, _, htlcId)) => htlcId }.toSet == Set(0) - if (isMissingHtlcIndex && commitmentFormat == DefaultCommitmentFormat) { + if (isMissingHtlcIndex && channelType.commitmentFormat == DefaultCommitmentFormat) { tx.txIn .map(_.witness) .collect(Scripts.extractPaymentHashFromHtlcTimeout) @@ -1044,15 +1044,15 @@ object Helpers { * @param tx a tx that has reached mindepth * @return a set of htlcs that need to be failed upstream */ - def timedOutHtlcs(commitmentFormat: CommitmentFormat, remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished, remoteDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { - val untrimmedHtlcs = Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec, commitmentFormat).map(_.add) + def timedOutHtlcs(channelType: SupportedChannelType, remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished, remoteDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { + val untrimmedHtlcs = Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec, channelType).map(_.add) if (tx.txid == remoteCommit.txid) { // the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx) remoteCommit.spec.htlcs.collect(incoming) -- untrimmedHtlcs } else { // maybe this is a timeout tx, in that case we can resolve and fail the corresponding htlc val isMissingHtlcIndex = remoteCommitPublished.claimHtlcTxs.values.collect { case Some(ClaimHtlcTimeoutTx(_, _, htlcId)) => htlcId }.toSet == Set(0) - if (isMissingHtlcIndex && commitmentFormat == DefaultCommitmentFormat) { + if (isMissingHtlcIndex && channelType.commitmentFormat == DefaultCommitmentFormat) { tx.txIn .map(_.witness) .collect(Scripts.extractPaymentHashFromClaimHtlcTimeout) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala index f70dafa4a7..481de0ebd0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala @@ -215,7 +215,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, } Behaviors.receiveMessagePartial { case PreconditionsOk => - val commitFeerate = cmd.commitments.localCommit.spec.feeratePerKw + val commitFeerate = cmd.commitments.localCommit.spec.commitTxFeerate if (targetFeerate <= commitFeerate) { log.info("skipping {}: commit feerate is high enough (feerate={})", cmd.desc, commitFeerate) // We set retry = true in case the on-chain feerate rises before the commit tx is confirmed: if that happens we'll @@ -239,11 +239,11 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, } def checkHtlcPreconditions(replyTo: ActorRef[TxPublisher.PublishTxResult], cmd: TxPublisher.PublishReplaceableTx, htlcTx: HtlcTx, targetFeerate: FeeratePerKw): Behavior[Command] = { - val commitFeerate = cmd.commitments.localCommit.spec.feeratePerKw + val htlcFeerate = cmd.commitments.localCommit.spec.htlcTxFeerate(cmd.commitments.channelType) // HTLC transactions have a 1-block relative delay when using anchor outputs, which ensures our commit tx has already // been confirmed (we don't need to check again here). HtlcTxAndWitnessData(htlcTx, cmd.commitments) match { - case Some(txWithWitnessData) if targetFeerate <= commitFeerate => + case Some(txWithWitnessData) if targetFeerate <= htlcFeerate => val channelKeyPath = keyManager.keyPath(cmd.commitments.localParams, cmd.commitments.channelConfig) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, cmd.commitments.localCommit.index) val localHtlcBasepoint = keyManager.htlcPoint(channelKeyPath) @@ -376,7 +376,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, private def addInputs(txInfo: ClaimLocalAnchorOutputTx, targetFeerate: FeeratePerKw, commitments: Commitments): Future[ClaimLocalAnchorOutputTx] = { val dustLimit = commitments.localParams.dustLimit - val commitFeerate = commitments.localCommit.spec.feeratePerKw + val commitFeerate = commitments.localCommit.spec.commitTxFeerate val commitTx = commitments.fullySignedLocalCommitTx(nodeParams.channelKeyManager).tx // We want the feerate of the package (commit tx + tx spending anchor) to equal targetFeerate. // Thus we have: anchorFeerate = targetFeerate + (weight-commit-tx / weight-anchor-tx) * (targetFeerate - commitTxFeerate) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index 79dcb2c3e5..abe1844d1f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -372,10 +372,10 @@ object PostRestartHtlcCleaner { val timedOutHtlcs: Set[Long] = (closingType_opt match { case Some(c: Closing.LocalClose) => val confirmedTxs = c.localCommitPublished.commitTx +: irrevocablySpent.filter(tx => Closing.isHtlcTimeout(tx, c.localCommitPublished)) - confirmedTxs.flatMap(tx => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx)) + confirmedTxs.flatMap(tx => Closing.timedOutHtlcs(d.commitments.channelType, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx)) case Some(c: Closing.RemoteClose) => val confirmedTxs = c.remoteCommitPublished.commitTx +: irrevocablySpent.filter(tx => Closing.isClaimHtlcTimeout(tx, c.remoteCommitPublished)) - confirmedTxs.flatMap(tx => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx)) + confirmedTxs.flatMap(tx => Closing.timedOutHtlcs(d.commitments.channelType, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx)) case _ => Seq.empty[UpdateAddHtlc] }).map(_.id).toSet overriddenHtlcs ++ timedOutHtlcs diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index 744d2c5fe8..8d7f5cb49d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -16,8 +16,10 @@ package fr.acinq.eclair.transactions +import fr.acinq.bitcoin.SatoshiLong import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.{ChannelTypes, SupportedChannelType} import fr.acinq.eclair.wire.protocol._ /** @@ -70,11 +72,17 @@ case class IncomingHtlc(add: UpdateAddHtlc) extends DirectedHtlc case class OutgoingHtlc(add: UpdateAddHtlc) extends DirectedHtlc -final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: FeeratePerKw, toLocal: MilliSatoshi, toRemote: MilliSatoshi) { +final case class CommitmentSpec(htlcs: Set[DirectedHtlc], commitTxFeerate: FeeratePerKw, toLocal: MilliSatoshi, toRemote: MilliSatoshi) { + + def htlcTxFeerate(channelType: SupportedChannelType): FeeratePerKw = channelType match { + case ChannelTypes.AnchorOutputsZeroFeeHtlcTx => FeeratePerKw(0 sat) + case _ => commitTxFeerate + } def findIncomingHtlcById(id: Long): Option[IncomingHtlc] = htlcs.collectFirst { case htlc: IncomingHtlc if htlc.add.id == id => htlc } def findOutgoingHtlcById(id: Long): Option[OutgoingHtlc] = htlcs.collectFirst { case htlc: OutgoingHtlc if htlc.add.id == id => htlc } + } object CommitmentSpec { @@ -140,7 +148,7 @@ object CommitmentSpec { case (spec, _) => spec } val spec5 = (localChanges ++ remoteChanges).foldLeft(spec4) { - case (spec, u: UpdateFee) => spec.copy(feeratePerKw = u.feeratePerKw) + case (spec, u: UpdateFee) => spec.copy(commitTxFeerate = u.feeratePerKw) case (spec, _) => spec } spec5 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 3f298fee1a..26f09b51ea 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 @@ -22,6 +22,7 @@ import fr.acinq.bitcoin.SigVersion._ import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.SupportedChannelType import fr.acinq.eclair.transactions.CommitmentOutput._ import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc @@ -187,22 +188,22 @@ object Transactions { def fee2rate(fee: Satoshi, weight: Int): FeeratePerKw = FeeratePerKw((fee * 1000L) / weight) /** Offered HTLCs below this amount will be trimmed. */ - def offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = - dustLimit + weight2fee(spec.feeratePerKw, commitmentFormat.htlcTimeoutWeight) + def offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): Satoshi = + dustLimit + weight2fee(spec.htlcTxFeerate(channelType), channelType.commitmentFormat.htlcTimeoutWeight) - def trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Seq[OutgoingHtlc] = { - val threshold = offeredHtlcTrimThreshold(dustLimit, spec, commitmentFormat) + def trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): Seq[OutgoingHtlc] = { + val threshold = offeredHtlcTrimThreshold(dustLimit, spec, channelType) spec.htlcs .collect { case o: OutgoingHtlc if o.add.amountMsat >= threshold => o } .toSeq } /** Received HTLCs below this amount will be trimmed. */ - def receivedHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = - dustLimit + weight2fee(spec.feeratePerKw, commitmentFormat.htlcSuccessWeight) + def receivedHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): Satoshi = + dustLimit + weight2fee(spec.htlcTxFeerate(channelType), channelType.commitmentFormat.htlcSuccessWeight) - def trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Seq[IncomingHtlc] = { - val threshold = receivedHtlcTrimThreshold(dustLimit, spec, commitmentFormat) + def trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): Seq[IncomingHtlc] = { + val threshold = receivedHtlcTrimThreshold(dustLimit, spec, channelType) spec.htlcs .collect { case i: IncomingHtlc if i.add.amountMsat >= threshold => i } .toSeq @@ -212,11 +213,11 @@ object Transactions { def htlcOutputFee(feeratePerKw: FeeratePerKw, commitmentFormat: CommitmentFormat): MilliSatoshi = weight2feeMsat(feeratePerKw, commitmentFormat.htlcOutputWeight) /** Fee paid by the commit tx (depends on which HTLCs will be trimmed). */ - def commitTxFeeMsat(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): MilliSatoshi = { - val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec, commitmentFormat) - val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec, commitmentFormat) - val weight = commitmentFormat.commitWeight + commitmentFormat.htlcOutputWeight * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) - weight2feeMsat(spec.feeratePerKw, weight) + def commitTxFeeMsat(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): MilliSatoshi = { + val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec, channelType) + val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec, channelType) + val weight = channelType.commitmentFormat.commitWeight + channelType.commitmentFormat.htlcOutputWeight * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) + weight2feeMsat(spec.commitTxFeerate, weight) } /** @@ -225,19 +226,19 @@ object Transactions { * If you are adding multiple fees together for example, you should always add them in MilliSatoshi and then round * down to Satoshi. */ - def commitTxTotalCostMsat(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): MilliSatoshi = { + def commitTxTotalCostMsat(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): MilliSatoshi = { // The funder pays the on-chain fee by deducing it from its main output. - val txFee = commitTxFeeMsat(dustLimit, spec, commitmentFormat) + val txFee = commitTxFeeMsat(dustLimit, spec, channelType) // When using anchor outputs, the funder pays for *both* anchors all the time, even if only one anchor is present. // This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the funder's main output. - val anchorsCost = commitmentFormat match { + val anchorsCost = channelType.commitmentFormat match { case DefaultCommitmentFormat => Satoshi(0) case AnchorOutputsCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 } txFee + anchorsCost } - def commitTxTotalCost(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = commitTxTotalCostMsat(dustLimit, spec, commitmentFormat).truncateToSatoshi + def commitTxTotalCost(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): Satoshi = commitTxTotalCostMsat(dustLimit, spec, channelType).truncateToSatoshi /** * @param commitTxNumber commit tx number @@ -325,25 +326,25 @@ object Transactions { localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, spec: CommitmentSpec, - commitmentFormat: CommitmentFormat): CommitmentOutputs = { + channelType: SupportedChannelType): CommitmentOutputs = { val outputs = collection.mutable.ArrayBuffer.empty[CommitmentOutputLink[CommitmentOutput]] - trimOfferedHtlcs(localDustLimit, spec, commitmentFormat).foreach { htlc => - val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), commitmentFormat) + trimOfferedHtlcs(localDustLimit, spec, channelType).foreach { htlc => + val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), channelType.commitmentFormat) outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, OutHtlc(htlc))) } - trimReceivedHtlcs(localDustLimit, spec, commitmentFormat).foreach { htlc => - val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry, commitmentFormat) + trimReceivedHtlcs(localDustLimit, spec, channelType).foreach { htlc => + val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry, channelType.commitmentFormat) outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) } val hasHtlcs = outputs.nonEmpty val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { - (spec.toLocal.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat), spec.toRemote.truncateToSatoshi) + (spec.toLocal.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, channelType), spec.toRemote.truncateToSatoshi) } else { - (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat)) + (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, channelType)) } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway if (toLocalAmount >= localDustLimit) { @@ -354,7 +355,7 @@ object Transactions { } if (toRemoteAmount >= localDustLimit) { - commitmentFormat match { + channelType.commitmentFormat match { case DefaultCommitmentFormat => outputs.append(CommitmentOutputLink( TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey)), pay2pkh(remotePaymentPubkey), @@ -366,7 +367,7 @@ object Transactions { } } - if (commitmentFormat == AnchorOutputsCommitmentFormat) { + if (channelType.commitmentFormat == AnchorOutputsCommitmentFormat) { if (toLocalAmount >= localDustLimit || hasHtlcs) { outputs.append(CommitmentOutputLink(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2wsh(anchor(localFundingPubkey))), anchor(localFundingPubkey), ToLocalAnchor)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala index 079edf3335..d4e81a5157 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala @@ -56,22 +56,22 @@ class FeeEstimatorSpec extends AnyFunSuite { feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2)) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, None) === defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === defaultMaxCommitFeerate / 2) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 2)) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, None) === defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === defaultMaxCommitFeerate) assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === overrideMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, None) === overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === overrideMaxCommitFeerate) val currentFeerates1 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2)) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2) val currentFeerates2 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 1.5)) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2)) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate) } test("fee difference too high") { @@ -110,7 +110,7 @@ class FeeEstimatorSpec extends AnyFunSuite { ) testCases.foreach { case (networkFeerate, proposedFeerate, expected) => assert(tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputs, networkFeerate, proposedFeerate) === expected) - assert(tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, networkFeerate, proposedFeerate) === expected) + assert(tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, networkFeerate, proposedFeerate) === expected) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index 616f1c7ac8..6741c4f864 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -29,7 +29,7 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha val standardChannel = ChannelFeatures() val staticRemoteKeyChannel = ChannelFeatures(Features.StaticRemoteKey) val anchorOutputsChannel = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs) - val anchorOutputsZeroFeeHtlcsChannel = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTxs) + val anchorOutputsZeroFeeHtlcsChannel = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) assert(!standardChannel.hasFeature(Features.StaticRemoteKey)) assert(!standardChannel.hasFeature(Features.AnchorOutputs)) @@ -43,12 +43,12 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha assert(anchorOutputsChannel.hasFeature(Features.StaticRemoteKey)) assert(anchorOutputsChannel.hasFeature(Features.AnchorOutputs)) - assert(!anchorOutputsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTxs)) + assert(!anchorOutputsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) assert(anchorOutputsChannel.commitmentFormat === Transactions.AnchorOutputsCommitmentFormat) assert(!anchorOutputsChannel.paysDirectlyToWallet) assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.StaticRemoteKey)) - assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTxs)) + assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) assert(!anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputs)) assert(anchorOutputsZeroFeeHtlcsChannel.commitmentFormat === Transactions.AnchorOutputsCommitmentFormat) assert(!anchorOutputsZeroFeeHtlcsChannel.paysDirectlyToWallet) @@ -66,12 +66,12 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelTypes.StaticRemoteKey), TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.StaticRemoteKey), TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.AnchorOutputs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional), ChannelTypes.AnchorOutputs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTxs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTxs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTxs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), ) for (testCase <- testCases) { @@ -84,7 +84,7 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha Features.empty -> ChannelTypes.Standard, Features(StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey, Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs, - Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Mandatory) -> ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, + Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory) -> ChannelTypes.AnchorOutputsZeroFeeHtlcTx, ) for ((features, expected) <- validChannelTypes) { assert(ChannelTypes.fromFeatures(features) === expected) @@ -97,11 +97,11 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory), - Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional), - Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Mandatory), - Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Optional), - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTxs -> Mandatory), - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Mandatory), + Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), + Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), + Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), ) for (features <- invalidChannelTypes) { @@ -117,8 +117,8 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, Wumbo)) assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, AnchorOutputs)) assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).activated === Set(StaticRemoteKey, AnchorOutputs, Wumbo)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, Features.empty, Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTxs)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).activated === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTxs, Wumbo)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty, Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).activated === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)) } } 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 cece0dc4f3..33201cd61b 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 @@ -148,6 +148,10 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat identifyHtlcs(setupHtlcs(Set(ChannelStateTestsTags.AnchorOutputs))) } + test("identify htlc txs (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { + identifyHtlcs(setupHtlcs(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))) + } + private def removeHtlcId(htlcTx: HtlcTx): HtlcTx = htlcTx match { case htlcTx: HtlcSuccessTx => htlcTx.copy(htlcId = 0) case htlcTx: HtlcTimeoutTx => htlcTx.copy(htlcId = 0) @@ -170,7 +174,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat import f._ val dustLimit = alice.underlyingActor.nodeParams.dustLimit - val commitmentFormat = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat + val channelType = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelType val localCommit = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.localCommit val remoteCommit = bob.stateData.asInstanceOf[DATA_CLOSING].commitments.remoteCommit @@ -185,25 +189,25 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(remoteCommitPublished) val aliceTimedOutHtlcs = htlcTimeoutTxs.map(htlcTimeout => { - val timedOutHtlcs = Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcTimeout.tx) + val timedOutHtlcs = Closing.timedOutHtlcs(channelType, localCommit, localCommitPublished, dustLimit, htlcTimeout.tx) assert(timedOutHtlcs.size === 1) timedOutHtlcs.head }) assert(aliceTimedOutHtlcs.toSet === aliceHtlcs) val bobTimedOutHtlcs = claimHtlcTimeoutTxs.map(claimHtlcTimeout => { - val timedOutHtlcs = Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcTimeout.tx) + val timedOutHtlcs = Closing.timedOutHtlcs(channelType, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcTimeout.tx) assert(timedOutHtlcs.size === 1) timedOutHtlcs.head }) assert(bobTimedOutHtlcs.toSet === bobHtlcs) - htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) - htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) - htlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, htlcTimeout.tx).isEmpty)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, claimHtlcTimeout.tx).isEmpty)) + htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.timedOutHtlcs(channelType, localCommit, localCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) + htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.timedOutHtlcs(channelType, remoteCommit, remoteCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) + claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.timedOutHtlcs(channelType, localCommit, localCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) + claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.timedOutHtlcs(channelType, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) + htlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.timedOutHtlcs(channelType, remoteCommit, remoteCommitPublished, dustLimit, htlcTimeout.tx).isEmpty)) + claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.timedOutHtlcs(channelType, localCommit, localCommitPublished, dustLimit, claimHtlcTimeout.tx).isEmpty)) } test("find timed out htlcs") { @@ -218,6 +222,10 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat findTimedOutHtlcs(setupHtlcs(Set(ChannelStateTestsTags.AnchorOutputs)), withoutHtlcId = false) } + test("find timed out htlcs (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { + findTimedOutHtlcs(setupHtlcs(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)), withoutHtlcId = false) + } + test("tell closing type") { val commitments = CommitmentsSpec.makeCommitments(10000 msat, 15000 msat) val tx1 :: tx2 :: tx3 :: tx4 :: tx5 :: tx6 :: Nil = List( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 146f397890..93ebe3d746 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -87,7 +87,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } // NB: we can't use ScalaTest's fixtures, they would see uninitialized bitcoind fields because they sandbox each test. - private def withFixture(utxos: Seq[BtcAmount], testFun: Fixture => Any): Unit = { + private def withFixture(utxos: Seq[BtcAmount], channelType: SupportedChannelType, testFun: Fixture => Any): Unit = { // Create a unique wallet for this test and ensure it has some btc. val testId = UUID.randomUUID() val walletRpcClient = createWallet(s"lightning-$testId") @@ -108,7 +108,12 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w blockCount.set(currentBlockHeight(probe)) val aliceNodeParams = TestConstants.Alice.nodeParams.copy(blockCount = blockCount) val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockCount = blockCount), bitcoinWallet) - reachNormal(setup, Set(ChannelStateTestsTags.AnchorOutputs)) + val testTags = channelType match { + case ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) + case ChannelTypes.AnchorOutputs => Set(ChannelStateTestsTags.AnchorOutputs) + case _ => fail("channel type must be a form of anchor outputs") + } + reachNormal(setup, testTags) import setup._ awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) @@ -143,10 +148,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate high enough, not spending anchor output") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ - val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.feeratePerKw + val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate val (_, anchorTx) = closeChannelWithoutHtlcs(f) publisher ! Publish(probe.ref, anchorTx, commitFeerate) @@ -157,7 +162,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f) @@ -173,10 +178,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate high enough and commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ - val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.feeratePerKw + val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f) walletClient.publishTransaction(commitTx.tx).pipeTo(probe.ref) probe.expectMsg(commitTx.tx.txid) @@ -190,7 +195,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -207,7 +212,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx published, not spending anchor output") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -225,11 +230,11 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx replaces local commit tx, not spending anchor output") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.feeratePerKw === FeeratePerKw(2500 sat)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate === FeeratePerKw(2500 sat)) // We lower the feerate to make it easy to replace our commit tx by theirs in the mempool. val lowFeerate = FeeratePerKw(500 sat) @@ -252,7 +257,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("not enough funds to increase commit tx feerate") { - withFixture(Seq(10.5 millibtc), f => { + withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ // close channel and wait for the commit tx to be published, anchor will not be published because we don't have enough funds @@ -270,7 +275,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate too low, spending anchor output") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f) @@ -298,7 +303,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not published, publishing it and spending anchor output") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f) @@ -332,7 +337,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w 22000 sat, 15000 sat ) - withFixture(utxos, f => { + withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val targetFeerate = FeeratePerKw(10_000 sat) @@ -358,7 +363,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock utxos when anchor tx cannot be published") { - withFixture(Seq(500 millibtc, 200 millibtc), f => { + withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val targetFeerate = FeeratePerKw(3000 sat) @@ -389,7 +394,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock anchor utxos when stopped before completion") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val targetFeerate = FeeratePerKw(3000 sat) @@ -407,8 +412,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("adjust anchor tx change amount", Tag("fuzzy")) { - withFixture(Seq(500 millibtc), f => { - val commitFeerate = f.alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.feeratePerKw + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { + val commitFeerate = f.alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate assert(commitFeerate < TestConstants.feeratePerKw) val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f) val anchorTxInfo = anchorTx.txInfo.asInstanceOf[ClaimLocalAnchorOutputTx] @@ -482,7 +487,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("not enough funds to increase htlc tx feerate") { - withFixture(Seq(10.5 millibtc), f => { + withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f) @@ -544,24 +549,39 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc tx feerate high enough, not adding wallet inputs") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputs, f => { import f._ - val currentFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.feeratePerKw + val currentFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, currentFeerate) + assert(htlcSuccess.txInfo.fee > 0.sat) assert(htlcSuccessTx.txIn.length === 1) val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, currentFeerate) + assert(htlcTimeout.txInfo.fee > 0.sat) assert(htlcTimeoutTx.txIn.length === 1) }) } test("htlc tx feerate too low, adding wallet inputs") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputs, f => { + val targetFeerate = FeeratePerKw(15_000 sat) + val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f) + val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, targetFeerate) + assert(htlcSuccessTx.txIn.length > 1) + val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, targetFeerate) + assert(htlcTimeoutTx.txIn.length > 1) + }) + } + + test("htlc tx feerate zero, adding wallet inputs") { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { val targetFeerate = FeeratePerKw(15_000 sat) val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f) + assert(htlcSuccess.txInfo.fee === 0.sat) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, targetFeerate) assert(htlcSuccessTx.txIn.length > 1) + assert(htlcTimeout.txInfo.fee === 0.sat) val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, targetFeerate) assert(htlcTimeoutTx.txIn.length > 1) }) @@ -583,7 +603,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w 5200 sat, 5100 sat ) - withFixture(utxos, f => { + withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { val targetFeerate = FeeratePerKw(8_000 sat) val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, targetFeerate) @@ -594,7 +614,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock utxos when htlc tx cannot be published") { - withFixture(Seq(500 millibtc, 200 millibtc), f => { + withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val targetFeerate = FeeratePerKw(5_000 sat) @@ -628,7 +648,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock htlc utxos when stopped before completion") { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { import f._ val targetFeerate = FeeratePerKw(5_000 sat) @@ -645,7 +665,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("adjust htlc tx change amount", Tag("fuzzy")) { - withFixture(Seq(500 millibtc), f => { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx, f => { val (_, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f) val commitments = htlcSuccess.commitments val dustLimit = commitments.localParams.dustLimit diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 29d482bbd3..c317d5cdd5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -123,14 +123,14 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTxs, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.OptionUpfrontShutdownScript, FeatureSupport.Optional)) val bobInitFeatures = Bob.nodeParams.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTxs, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.OptionUpfrontShutdownScript, FeatureSupport.Optional)) @@ -285,7 +285,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { r2s.forward(s) s2r.expectMsgType[RevokeAndAck] s2r.forward(r) - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.spec.feeratePerKw == feerate) + awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.spec.commitTxFeerate == feerate) } def mutualClose(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, s2blockchain: TestProbe, r2blockchain: TestProbe): Unit = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 7b29c2a936..3fcccb6e1e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -62,7 +62,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { @@ -98,10 +98,10 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs)) + assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) } test("recv AcceptChannel (channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 285115d8ed..caeb6da5a5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -51,7 +51,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { @@ -86,10 +86,10 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv OpenChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs)) + assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) } test("recv OpenChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 697316bb63..878030d1ae 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -749,7 +749,6 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_SIGN (going above reserve)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => import f._ - val sender = TestProbe() // channel starts with all funds on alice's side, so channel will be initially disabled on bob's side assert(!bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags.isEnabled) // alice will send enough funds to bob to make it go above reserve @@ -829,30 +828,85 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - addHtlc(8000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - addHtlc(300000 msat, bob, alice, bob2alice, alice2bob) // b->a (dust) - addHtlc(1000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) + addHtlc(80000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) + addHtlc(1200000 msat, bob, alice, bob2alice, alice2bob) // b->a (trimmed to dust) + addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) - addHtlc(500000 msat, alice, bob, alice2bob, bob2alice) // a->b (dust) - addHtlc(4000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) + addHtlc(1200000 msat, alice, bob, alice2bob, bob2alice) // a->b (trimmed to dust) + addHtlc(40000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) alice ! CMD_SIGN() - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) + val aliceCommitSig = alice2bob.expectMsgType[CommitSig] + assert(aliceCommitSig.htlcSignatures.length === 3) + alice2bob.forward(bob, aliceCommitSig) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // actual test begins - bob2alice.expectMsgType[CommitSig] + val bobCommitSig = bob2alice.expectMsgType[CommitSig] + assert(bobCommitSig.htlcSignatures.length === 5) + bob2alice.forward(alice, bobCommitSig) + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs.size == 5) + } + + test("recv CommitSig (multiple htlcs in both directions) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + import f._ + + addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) + addHtlc(1100000 msat, alice, bob, alice2bob, bob2alice) // a->b (trimmed to dust) + addHtlc(999999 msat, bob, alice, bob2alice, alice2bob) // b->a (dust) + addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) + addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) + addHtlc(999999 msat, alice, bob, alice2bob, bob2alice) // a->b (dust) + addHtlc(1100000 msat, bob, alice, bob2alice, alice2bob) // b->a (trimmed to dust) + + alice ! CMD_SIGN() + val aliceCommitSig = alice2bob.expectMsgType[CommitSig] + assert(aliceCommitSig.htlcSignatures.length === 2) + alice2bob.forward(bob, aliceCommitSig) + bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) + // actual test begins + val bobCommitSig = bob2alice.expectMsgType[CommitSig] + assert(bobCommitSig.htlcSignatures.length === 3) + bob2alice.forward(alice, bobCommitSig) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs.size == 3) } + test("recv CommitSig (multiple htlcs in both directions) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) + addHtlc(1100000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) + addHtlc(999999 msat, bob, alice, bob2alice, alice2bob) // b->a (dust) + addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) + addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) + addHtlc(999999 msat, alice, bob, alice2bob, bob2alice) // a->b (dust) + addHtlc(1100000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) + + alice ! CMD_SIGN() + val aliceCommitSig = alice2bob.expectMsgType[CommitSig] + assert(aliceCommitSig.htlcSignatures.length === 3) + alice2bob.forward(bob, aliceCommitSig) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + + // actual test begins + val bobCommitSig = bob2alice.expectMsgType[CommitSig] + assert(bobCommitSig.htlcSignatures.length === 5) + bob2alice.forward(alice, bobCommitSig) + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs.size == 5) + } + test("recv CommitSig (only fee update)") { f => import f._ - val sender = TestProbe() alice ! CMD_UPDATE_FEE(TestConstants.feeratePerKw + FeeratePerKw(1000 sat), commit = false) alice ! CMD_SIGN() @@ -1178,7 +1232,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testRevokeAndAckHtlcStaticRemoteKey _ } - test("recv RevokeAndAck (one htlc sent, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { + test("recv RevokeAndAck (one htlc sent, anchor_outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { + testRevokeAndAckHtlcStaticRemoteKey _ + } + + test("recv RevokeAndAck (one htlc sent, anchors_zero_fee_htlc_tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { testRevokeAndAckHtlcStaticRemoteKey _ } @@ -1224,6 +1282,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testReceiveCmdFulfillHtlc _ } + test("recv CMD_FULFILL_HTLC (anchors_zero_fee_htlc_tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { + testReceiveCmdFulfillHtlc _ + } + test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() @@ -1301,7 +1363,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testUpdateFulfillHtlc _ } - test("recv UpdateFulfillHtlc (static_remotekey)", Tag("(static_remotekey)")) { + test("recv UpdateFulfillHtlc (static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { testUpdateFulfillHtlc _ } @@ -1309,6 +1371,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testUpdateFulfillHtlc _ } + test("recv UpdateFulfillHtlc (anchors_zero_fee_htlc_tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { + testUpdateFulfillHtlc _ + } + test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f => import f._ val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1388,6 +1454,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testCmdFailHtlc _ } + test("recv CMD_FAIL_HTLC (anchors_zero_fee_htlc_tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { + testCmdFailHtlc _ + } + test("recv CMD_FAIL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() @@ -1503,6 +1573,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testUpdateFailHtlc _ } + test("recv UpdateFailHtlc (anchors_zero_fee_htlc_tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { + testUpdateFailHtlc _ + } + test("recv UpdateFailMalformedHtlc") { f => import f._ @@ -1605,7 +1679,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testCmdUpdateFee _ } - test("recv CMD_UPDATE_FEE (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { + test("recv CMD_UPDATE_FEE (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { testCmdUpdateFee _ } @@ -1639,10 +1713,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee), remoteNextHtlcId = 0))) } - test("recv UpdateFee (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv UpdateFee (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialData.commitments.localCommit.spec.feeratePerKw === TestConstants.anchorOutputsFeeratePerKw) + assert(initialData.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) val fee = UpdateFee(ByteVector32.Zeroes, TestConstants.anchorOutputsFeeratePerKw * 0.8) bob ! fee awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee), remoteNextHtlcId = 0))) @@ -1688,7 +1762,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectMsgType[WatchTxConfirmed] } - test("recv UpdateFee (sender can't afford it, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv UpdateFee (sender can't afford it, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx // This feerate is just above the threshold: (800000 (alice balance) - 20000 (reserve) - 660 (anchors)) / 1124 (commit tx weight) = 693363 @@ -1706,7 +1780,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val commitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(initialState.commitments.localCommit.spec.feeratePerKw === TestConstants.feeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.feeratePerKw) alice2bob.send(bob, UpdateFee(ByteVector32.Zeroes, TestConstants.feeratePerKw * 3)) bob2alice.expectNoMessage(250 millis) // we don't close because the commitment doesn't contain any HTLC @@ -1722,12 +1796,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectMsgType[WatchTxConfirmed] } - test("recv UpdateFee (remote feerate is too high, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv UpdateFee (remote feerate is too high, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val commitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(initialState.commitments.localCommit.spec.feeratePerKw === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) alice2bob.send(bob, UpdateFee(initialState.channelId, TestConstants.anchorOutputsFeeratePerKw * 3)) bob2alice.expectNoMessage(250 millis) // we don't close because the commitment doesn't contain any HTLC @@ -1741,12 +1815,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === commitTx.txid) } - test("recv UpdateFee (remote feerate is too small, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv UpdateFee (remote feerate is too small, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val commitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(initialState.commitments.localCommit.spec.feeratePerKw === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) alice2bob.send(bob, UpdateFee(initialState.channelId, FeeratePerKw(FeeratePerByte(2 sat)))) bob2alice.expectNoMessage(250 millis) // we don't close because the commitment doesn't contain any HTLC @@ -1765,7 +1839,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments val tx = bobCommitments.localCommit.commitTxAndRemoteSig.commitTx.tx val expectedFeeratePerKw = bob.feeEstimator.getFeeratePerKw(bob.feeTargets.commitmentBlockTarget) - assert(bobCommitments.localCommit.spec.feeratePerKw == expectedFeeratePerKw) + assert(bobCommitments.localCommit.spec.commitTxFeerate == expectedFeeratePerKw) bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === "remote fee rate is too small: remoteFeeratePerKw=252") @@ -1813,6 +1887,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testCmdClose(f, None) } + test("recv CMD_CLOSE (no pending htlcs) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testCmdClose(f, None) + } + test("recv CMD_CLOSE (with noSender)") { f => import f._ val sender = TestProbe() @@ -1982,6 +2060,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testShutdown(f, None) } + test("recv Shutdown (no pending htlcs) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testShutdown(f, None) + } + test("recv Shutdown (with unacked sent htlcs)") { f => import f._ val sender = TestProbe() @@ -2117,6 +2199,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testShutdownWithHtlcs _ } + test("recv Shutdown (with signed htlcs) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { + testShutdownWithHtlcs _ + } + test("recv Shutdown (while waiting for a RevokeAndAck)") { f => import f._ val sender = TestProbe() @@ -2310,10 +2396,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.feePerBlock(Alice.nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget))) } - test("recv CurrentFeerate (when funder, triggers an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv CurrentFeerate (when funder, triggers an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommit.spec.feeratePerKw === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw / 2)) alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, TestConstants.anchorOutputsFeeratePerKw / 2)) } @@ -2325,10 +2411,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectNoMessage(500 millis) } - test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommit.spec.feeratePerKw === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2)) alice2bob.expectNoMessage(500 millis) } @@ -2356,7 +2442,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == CLOSING) } - test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different, with HTLCs, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different, with HTLCs, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ // We start with a feerate lower than the 10 sat/byte threshold. @@ -2367,7 +2453,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob) addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.feeratePerKw === TestConstants.anchorOutputsFeeratePerKw / 2) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw / 2) // The network fees spike, so Bob closes the channel. bob.feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 3045868e11..a9ded7b180 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -586,7 +586,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceCommitTx = aliceStateData.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - val currentFeeratePerKw = aliceStateData.commitments.localCommit.spec.feeratePerKw + val currentFeeratePerKw = aliceStateData.commitments.localCommit.spec.commitTxFeerate // we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1 // to ensure the network's feerate is 10% above our threshold). val networkFeeratePerKw = currentFeeratePerKw * (1.1 / alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(Bob.nodeParams.nodeId).ratioLow) @@ -612,7 +612,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with disconnect(alice, bob) val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL] - val currentFeeratePerKw = aliceStateData.commitments.localCommit.spec.feeratePerKw + val currentFeeratePerKw = aliceStateData.commitments.localCommit.spec.commitTxFeerate // we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1 // to ensure the network's feerate is 10% above our threshold). val networkFeeratePerKw = currentFeeratePerKw * (1.1 / alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(Bob.nodeParams.nodeId).ratioLow) @@ -630,7 +630,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // we simulate a disconnection disconnect(alice, bob) - val localFeeratePerKw = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.feeratePerKw + val localFeeratePerKw = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate val networkFeeratePerKw = localFeeratePerKw * 2 val networkFeerate = FeeratesPerKw.single(networkFeeratePerKw) @@ -695,7 +695,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val bobStateData = bob.stateData.asInstanceOf[DATA_NORMAL] val bobCommitTx = bobStateData.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - val currentFeeratePerKw = bobStateData.commitments.localCommit.spec.feeratePerKw + val currentFeeratePerKw = bobStateData.commitments.localCommit.spec.commitTxFeerate // we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1 // to ensure the network's feerate is 10% above our threshold). val networkFeeratePerKw = currentFeeratePerKw * (1.1 / bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(Alice.nodeParams.nodeId).ratioLow) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index f1bcfb1f4d..4f3d54852f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -630,10 +630,10 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.feePerBlock(alice.feeTargets.commitmentBlockTarget))) } - test("recv CurrentFeerate (when funder, triggers an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv CurrentFeerate (when funder, triggers an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - assert(initialState.commitments.localCommit.spec.feeratePerKw === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw / 2)) alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, TestConstants.anchorOutputsFeeratePerKw / 2)) } @@ -645,10 +645,10 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice2bob.expectNoMessage(500 millis) } - test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - assert(initialState.commitments.localCommit.spec.feeratePerKw === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2)) alice2bob.expectNoMessage(500 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 5b83df3069..85384b44ef 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -666,11 +666,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } - test("recv WatchTxConfirmedTriggered (remote commit, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv WatchTxConfirmedTriggered (remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + assert(initialState.commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors @@ -701,7 +701,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the latest commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - if (channelFeatures.hasFeature(Features.AnchorOutputs)) { + if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs } else { assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs @@ -735,6 +735,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testRemoteCommitTxWithHtlcsConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) } + test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testRemoteCommitTxWithHtlcsConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + } + test("recv WatchTxConfirmedTriggered (remote commit) followed by CMD_FULFILL_HTLC") { f => import f._ // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. @@ -829,7 +833,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the next commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - if (channelFeatures.hasFeature(Features.AnchorOutputs)) { + if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs } else { assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs @@ -879,9 +883,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } - test("recv WatchTxConfirmedTriggered (next remote commit, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv WatchTxConfirmedTriggered (next remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) alice ! WatchTxConfirmedTriggered(42, 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(45, 0, closingState.claimMainOutputTx.get.tx) @@ -949,10 +953,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with relayerA.expectNoMessage(100 millis) } - test("recv INPUT_RESTORED (next remote commit, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv INPUT_RESTORED (next remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val (bobCommitTx, closingState, _) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + val (bobCommitTx, closingState, _) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState) // simulate a node restart @@ -1009,7 +1013,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) // bob is nice and publishes its commitment val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - if (channelFeatures.hasFeature(Features.AnchorOutputs)) { + if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { assert(bobCommitTx.txOut.length === 6) // two main outputs + two anchors + 2 HTLCs } else { assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs @@ -1045,9 +1049,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED, 10 seconds) } - test("recv WatchTxConfirmedTriggered (future remote commit, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv WatchTxConfirmedTriggered (future remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) // alice is able to claim its main output val claimMainTx = alice2blockchain.expectMsgType[PublishRawTx].tx Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -1087,7 +1091,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob's first commit tx doesn't contain any htlc val localCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit - if (channelFeatures.hasFeature(Features.AnchorOutputs)) { + if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) // 2 main outputs + 2 anchors } else { assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) // 2 main outputs @@ -1104,7 +1108,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size) - if (channelFeatures.hasFeature(Features.AnchorOutputs)) { + if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) } else { assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) @@ -1121,7 +1125,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size) - if (channelFeatures.hasFeature(Features.AnchorOutputs)) { + if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8) } else { assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) @@ -1136,7 +1140,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size) - if (channelFeatures.hasFeature(Features.AnchorOutputs)) { + if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) } else { assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) @@ -1180,7 +1184,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice spends all outpoints of the revoked tx, except her main output when it goes directly to our wallet val spentOutpoints = penaltyTxs.flatMap(_.tx.txIn.map(_.outPoint)).toSet assert(spentOutpoints.forall(_.txid === bobRevokedTx.txid)) - if (channelFeatures.hasFeature(Features.AnchorOutputs)) { + if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { assert(spentOutpoints.size === bobRevokedTx.txOut.size - 2) // we don't claim the anchors } else if (channelFeatures.paysDirectlyToWallet) { @@ -1232,6 +1236,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testFundingSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) } + test("recv WatchFundingSpentTriggered (one revoked tx, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testFundingSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + } + test("recv WatchFundingSpentTriggered (multiple revoked tx)") { f => import f._ val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures()) @@ -1312,6 +1320,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testInputRestoredRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) } + test("recv INPUT_RESTORED (one revoked tx, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testInputRestoredRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + } + def testOutputSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ val revokedCloseFixture = prepareRevokedClose(f, channelFeatures) @@ -1429,11 +1441,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testOutputSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) } - test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published aggregated htlc tx)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published htlc-success tx, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testOutputSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + } + + test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published aggregated htlc tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ // bob publishes one of his revoked txs - val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2) val commitmentFormat = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitmentFormat alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) @@ -1510,7 +1526,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with private def testRevokedTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) - val initOutputCount = if (channelFeatures.hasFeature(Features.AnchorOutputs)) 4 else 2 + val initOutputCount = if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) 4 else 2 assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size === initOutputCount) // bob's second commit tx contains 2 incoming htlcs @@ -1560,6 +1576,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testRevokedTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) } + test("recv WatchTxConfirmedTriggered (one revoked tx, pending htlcs, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testRevokedTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + } + test("recv ChannelReestablish") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index f6feb733ba..5f106b175e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -629,15 +629,9 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { } -class AnchorOutputChannelIntegrationSpec extends ChannelIntegrationSpec { +abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { - test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29750, "eclair.api.port" -> 28095).asJava).withFallback(commonFeatures).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29751, "eclair.api.port" -> 28096).asJava).withFallback(withAnchorOutputs).withFallback(withWumbo).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29753, "eclair.api.port" -> 28097).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) - } - - test("connect nodes") { + def connectNodes(expectedChannelType: SupportedChannelType): Unit = { // A --- C --- F val eventListener = TestProbe() nodes("A").system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]) @@ -649,16 +643,21 @@ class AnchorOutputChannelIntegrationSpec extends ChannelIntegrationSpec { within(60 seconds) { var count = 0 while (count < 2) { - if (eventListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == NORMAL) count = count + 1 + val stateEvent = eventListener.expectMsgType[ChannelStateChanged](max = 60 seconds) + if (stateEvent.currentState == NORMAL) { + assert(stateEvent.commitments_opt.nonEmpty) + assert(stateEvent.commitments_opt.get.asInstanceOf[Commitments].channelType === expectedChannelType) + count = count + 1 + } } } - // generate more blocks so that all funding txes are buried under at least 6 blocks + // generate more blocks so that all funding txs are buried under at least 6 blocks generateBlocks(4) awaitAnnouncements(1) } - test("open channel C <-> F, send payments and close (option_anchor_outputs, option_static_remotekey)") { + def testOpenPayClose(expectedChannelType: SupportedChannelType): Unit = { connect(nodes("C"), nodes("F"), 5000000 sat, 0 msat) generateBlocks(6) awaitAnnouncements(2) @@ -671,6 +670,7 @@ class AnchorOutputChannelIntegrationSpec extends ChannelIntegrationSpec { sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) val initialStateDataF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data + assert(initialStateDataF.commitments.channelType === expectedChannelType) val initialCommitmentIndex = initialStateDataF.commitments.localCommit.index // the 'to remote' address is a simple script spending to the remote payment basepoint with a 1-block CSV delay @@ -745,23 +745,7 @@ class AnchorOutputChannelIntegrationSpec extends ChannelIntegrationSpec { awaitAnnouncements(1) } - test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs)") { - testDownstreamFulfillLocalCommit(Transactions.AnchorOutputsCommitmentFormat) - } - - test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs)") { - testDownstreamFulfillRemoteCommit(Transactions.AnchorOutputsCommitmentFormat) - } - - test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs)") { - testDownstreamTimeoutLocalCommit(Transactions.AnchorOutputsCommitmentFormat) - } - - test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs)") { - testDownstreamTimeoutRemoteCommit(Transactions.AnchorOutputsCommitmentFormat) - } - - test("punish a node that has published a revoked commit tx (anchor outputs)") { + def testPunishRevokedCommit(): Unit = { val revokedCommitFixture = testRevokedCommit(Transactions.AnchorOutputsCommitmentFormat) import revokedCommitFixture._ @@ -790,4 +774,80 @@ class AnchorOutputChannelIntegrationSpec extends ChannelIntegrationSpec { awaitAnnouncements(1) } -} \ No newline at end of file +} + +class AnchorOutputChannelIntegrationSpec extends AnchorChannelIntegrationSpec { + + test("start eclair nodes") { + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29750, "eclair.api.port" -> 28093).asJava).withFallback(commonFeatures).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29751, "eclair.api.port" -> 28094).asJava).withFallback(withAnchorOutputs).withFallback(withWumbo).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29753, "eclair.api.port" -> 28095).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) + } + + test("connect nodes") { + connectNodes(ChannelTypes.StaticRemoteKey) + } + + test("open channel C <-> F, send payments and close (anchor outputs)") { + testOpenPayClose(ChannelTypes.AnchorOutputs) + } + + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs)") { + testDownstreamFulfillLocalCommit(Transactions.AnchorOutputsCommitmentFormat) + } + + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs)") { + testDownstreamFulfillRemoteCommit(Transactions.AnchorOutputsCommitmentFormat) + } + + test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs)") { + testDownstreamTimeoutLocalCommit(Transactions.AnchorOutputsCommitmentFormat) + } + + test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs)") { + testDownstreamTimeoutRemoteCommit(Transactions.AnchorOutputsCommitmentFormat) + } + + test("punish a node that has published a revoked commit tx (anchor outputs)") { + testPunishRevokedCommit() + } + +} + +class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelIntegrationSpec { + + test("start eclair nodes") { + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29760, "eclair.api.port" -> 28096).asJava).withFallback(commonFeatures).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29761, "eclair.api.port" -> 28097).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(withWumbo).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29763, "eclair.api.port" -> 28098).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + } + + test("connect nodes") { + connectNodes(ChannelTypes.StaticRemoteKey) + } + + test("open channel C <-> F, send payments and close (anchor outputs zero fee htlc txs)") { + testOpenPayClose(ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + } + + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs zero fee htlc txs)") { + testDownstreamFulfillLocalCommit(Transactions.AnchorOutputsCommitmentFormat) + } + + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs zero fee htlc txs)") { + testDownstreamFulfillRemoteCommit(Transactions.AnchorOutputsCommitmentFormat) + } + + test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs zero fee htlc txs)") { + testDownstreamTimeoutLocalCommit(Transactions.AnchorOutputsCommitmentFormat) + } + + test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs zero fee htlc txs)") { + testDownstreamTimeoutRemoteCommit(Transactions.AnchorOutputsCommitmentFormat) + } + + test("punish a node that has published a revoked commit tx (anchor outputs)") { + testPunishRevokedCommit() + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 9ffe0d92a0..6d0527a097 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -111,6 +111,10 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit s"eclair.features.${AnchorOutputs.rfcName}" -> "optional" ).asJava)) + val withAnchorOutputsZeroFeeHtlcTxs = withAnchorOutputs.withFallback(ConfigFactory.parseMap(Map( + s"eclair.features.${AnchorOutputsZeroFeeHtlcTx.rfcName}" -> "optional" + ).asJava)) + implicit val formats: Formats = DefaultFormats override def beforeAll(): Unit = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index f6abff703f..8c91ada472 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -61,9 +61,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { test("start eclair nodes") { instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel-flags" -> 0).asJava).withFallback(commonFeatures).withFallback(commonConfig)) // A's channels are private instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonFeatures).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputs).withFallback(withWumbo).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(withWumbo).withFallback(commonConfig)) instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonFeatures).withFallback(commonConfig)) - instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) + instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withWumbo).withFallback(commonConfig)) instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.fee-base-msat" -> 1010, "eclair.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala index d3cec352d8..6056097a9f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala @@ -136,14 +136,14 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging s" Received htlcs: ${localCommit.spec.htlcs.collect { case IncomingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", s" Balance us: ${localCommit.spec.toLocal}", s" Balance them: ${localCommit.spec.toRemote}", - s" Fee rate: ${localCommit.spec.feeratePerKw.toLong}", + s" Fee rate: ${localCommit.spec.commitTxFeerate.toLong}", "REMOTE COMMITS:", s" Commit ${remoteCommit.index}:", s" Offered htlcs: ${remoteCommit.spec.htlcs.collect { case OutgoingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", s" Received htlcs: ${remoteCommit.spec.htlcs.collect { case IncomingHtlc(add) => (add.id, add.amountMsat) }.mkString(" ")}", s" Balance us: ${remoteCommit.spec.toLocal}", s" Balance them: ${remoteCommit.spec.toRemote}", - s" Fee rate: ${remoteCommit.spec.feeratePerKw.toLong}") + s" Fee rate: ${remoteCommit.spec.commitTxFeerate.toLong}") .foreach(s => { fout.write(rtrim(s)) fout.newLine() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 7c14001513..909b584201 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -23,7 +23,7 @@ import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, Btc, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} -import fr.acinq.eclair.Features.{AnchorOutputs, AnchorOutputsZeroFeeHtlcTxs, StaticRemoteKey, Wumbo} +import fr.acinq.eclair.Features.{AnchorOutputs, AnchorOutputsZeroFeeHtlcTx, StaticRemoteKey, Wumbo} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw @@ -68,7 +68,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle .modify(_.features).setToIf(test.tags.contains("static_remotekey"))(Features(StaticRemoteKey -> Optional)) .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Wumbo -> Optional)) .modify(_.features).setToIf(test.tags.contains("anchor_outputs"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional)) - .modify(_.features).setToIf(test.tags.contains("anchor_outputs_zero_fee_htlc_tx"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional)) + .modify(_.features).setToIf(test.tags.contains("anchor_outputs_zero_fee_htlc_tx"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional)) .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) @@ -320,7 +320,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle } // They only support anchor outputs with zero fee htlc txs and we don't. { - val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs)) + val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) val open = protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0, openTlv) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs_zero_fee_htlc_tx, expected channel_type=standard")) @@ -396,7 +396,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle import f._ val probe = TestProbe() - connect(remoteNodeId, peer, peerConnection, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTxs -> Optional))) + connect(remoteNodeId, peer, peerConnection, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional))) assert(peer.stateData.channels.isEmpty) // We ensure the current network feerate is higher than the default anchor output feerate. @@ -404,7 +404,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2)) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) + assert(init.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) assert(init.fundingAmount === 15000.sat) assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw) assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala index 85e897c81b..6e43d76b4e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala @@ -18,13 +18,15 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.{ByteVector32, Crypto, SatoshiLong} import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelTypes import fr.acinq.eclair.wire.protocol.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong, TestConstants, randomBytes32} import org.scalatest.funsuite.AnyFunSuite class CommitmentSpecSpec extends AnyFunSuite { + test("add, fulfill and fail htlcs from the sender side") { - val spec = CommitmentSpec(htlcs = Set(), feeratePerKw = FeeratePerKw(1000 sat), toLocal = 5000000 msat, toRemote = 0 msat) + val spec = CommitmentSpec(htlcs = Set(), commitTxFeerate = FeeratePerKw(1000 sat), toLocal = 5000000 msat, toRemote = 0 msat) val R = randomBytes32() val H = Crypto.sha256(R) @@ -46,13 +48,13 @@ class CommitmentSpecSpec extends AnyFunSuite { } test("add, fulfill and fail htlcs from the receiver side") { - val spec = CommitmentSpec(htlcs = Set(), feeratePerKw = FeeratePerKw(1000 sat), toLocal = 0 msat, toRemote = (5000 * 1000) msat) + val spec = CommitmentSpec(htlcs = Set(), commitTxFeerate = FeeratePerKw(1000 sat), toLocal = 0 msat, toRemote = (5000 * 1000) msat) val R = randomBytes32() val H = Crypto.sha256(R) val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, Nil, add1 :: Nil) - assert(spec1 === spec.copy(htlcs = Set(IncomingHtlc(add1)), toRemote = (3000 * 1000 msat))) + assert(spec1 === spec.copy(htlcs = Set(IncomingHtlc(add1)), toRemote = 3000 * 1000 msat)) val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, Nil, add2 :: Nil) @@ -66,4 +68,13 @@ class CommitmentSpecSpec extends AnyFunSuite { val spec4 = CommitmentSpec.reduce(spec3, fail1 :: Nil, Nil) assert(spec4 === spec3.copy(htlcs = Set(), toRemote = (3000 * 1000) msat)) } + + test("compute htlc tx feerate based on channel type") { + val spec = CommitmentSpec(htlcs = Set(), commitTxFeerate = FeeratePerKw(2500 sat), toLocal = (5000 * 1000) msat, toRemote = (2500 * 1000) msat) + assert(spec.htlcTxFeerate(ChannelTypes.Standard) === FeeratePerKw(2500 sat)) + assert(spec.htlcTxFeerate(ChannelTypes.StaticRemoteKey) === FeeratePerKw(2500 sat)) + assert(spec.htlcTxFeerate(ChannelTypes.AnchorOutputs) === FeeratePerKw(2500 sat)) + assert(spec.htlcTxFeerate(ChannelTypes.AnchorOutputsZeroFeeHtlcTx) === FeeratePerKw(0 sat)) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index 732e69e765..4c5808ecfa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, TestConstants} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, TestConstants} import grizzled.slf4j.Logging import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -36,9 +36,11 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { // @formatter:off def filename: String def channelFeatures: ChannelFeatures - def commitmentFormat: CommitmentFormat // @formatter:on + val channelType = channelFeatures.channelType + val commitmentFormat = channelType.commitmentFormat + val tests = { val tests = collection.mutable.HashMap.empty[String, Map[String, String]] val current = collection.mutable.HashMap.empty[String, String] @@ -181,7 +183,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val spec = CommitmentSpec(specHtlcs, getFeerate(name), getToLocal(name), getToRemote(name)) logger.info(s"to_local_msat: ${spec.toLocal}") logger.info(s"to_remote_msat: ${spec.toRemote}") - logger.info(s"local_feerate_per_kw: ${spec.feeratePerKw}") + logger.info(s"local_feerate_per_kw: ${spec.commitTxFeerate}") val outputs = Transactions.makeCommitTxOutputs( localIsFunder = true, @@ -195,7 +197,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { localFundingPubkey = Local.funding_pubkey, remoteFundingPubkey = Remote.funding_pubkey, spec, - commitmentFormat) + channelType) val commitTx = { val tx = Transactions.makeCommitTx( @@ -212,7 +214,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { Transactions.addSigs(tx, Local.funding_pubkey, Remote.funding_pubkey, local_sig, remote_sig) } - val baseFee = Transactions.commitTxFeeMsat(Local.dustLimit, spec, commitmentFormat) + val baseFee = Transactions.commitTxFeeMsat(Local.dustLimit, spec, channelType) logger.info(s"# base commitment transaction fee = ${baseFee.toLong}") val actualFee = fundingAmount - commitTx.tx.txOut.map(_.amount).sum logger.info(s"# actual commitment transaction fee = ${actualFee.toLong}") @@ -235,7 +237,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { Local.dustLimit, Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey, - spec.feeratePerKw, + spec.htlcTxFeerate(channelType), outputs, commitmentFormat) @@ -400,7 +402,6 @@ class DefaultCommitmentTestVectorSpec extends TestVectorsSpec { // @formatter:off override def filename: String = "/bolt3-tx-test-vectors-default-commitment-format.txt" override def channelFeatures: ChannelFeatures = ChannelFeatures() - override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat // @formatter:on } @@ -408,7 +409,6 @@ class StaticRemoteKeyTestVectorSpec extends TestVectorsSpec { // @formatter:off override def filename: String = "/bolt3-tx-test-vectors-static-remotekey-format.txt" override def channelFeatures: ChannelFeatures = ChannelFeatures(Features.StaticRemoteKey) - override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat // @formatter:on } @@ -416,6 +416,5 @@ class AnchorOutputsTestVectorSpec extends TestVectorsSpec { // @formatter:off override def filename: String = "/bolt3-tx-test-vectors-anchor-outputs-format.txt" override def channelFeatures: ChannelFeatures = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs) - override def commitmentFormat: CommitmentFormat = AnchorOutputsCommitmentFormat // @formatter:on } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 1df91263e8..6efbde534f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -21,11 +21,12 @@ import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} import fr.acinq.bitcoin.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, SIGHASH_ALL, SIGHASH_ANYONECANPAY, SIGHASH_NONE, SIGHASH_SINGLE, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, millibtc2satoshi} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelTypes import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc} import fr.acinq.eclair.transactions.Scripts.{anchor, htlcOffered, htlcReceived, toLocalDelayed} import fr.acinq.eclair.transactions.Transactions.AnchorOutputsCommitmentFormat.anchorAmount -import fr.acinq.eclair.transactions.Transactions.{addSigs, _} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import grizzled.slf4j.Logging import org.scalatest.funsuite.AnyFunSuite @@ -103,8 +104,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 7000000 msat, ByteVector32.Zeroes, CltvExpiry(550), TestConstants.emptyOnionPacket)), IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 800000 msat, ByteVector32.Zeroes, CltvExpiry(551), TestConstants.emptyOnionPacket)) ) - val spec = CommitmentSpec(htlcs, feeratePerKw = FeeratePerKw(5000 sat), toLocal = 0 msat, toRemote = 0 msat) - val fee = commitTxFeeMsat(546 sat, spec, DefaultCommitmentFormat) + val spec = CommitmentSpec(htlcs, FeeratePerKw(5000 sat), toLocal = 0 msat, toRemote = 0 msat) + val fee = commitTxFeeMsat(546 sat, spec, ChannelTypes.Standard) assert(fee === 5340000.msat) } @@ -168,7 +169,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val paymentPreimage = randomBytes32() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val spec = CommitmentSpec(Set(OutgoingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.Standard) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), DefaultCommitmentFormat))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) @@ -183,7 +184,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val paymentPreimage = randomBytes32() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), toLocalDelay.toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val spec = CommitmentSpec(Set(IncomingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.Standard) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry, DefaultCommitmentFormat))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(claimClaimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) @@ -213,38 +214,38 @@ class TransactionsSpec extends AnyFunSuite with Logging { } test("generate valid commitment with some outputs that don't materialize (default commitment format)") { - val spec = CommitmentSpec(htlcs = Set.empty, feeratePerKw = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) - val commitFee = commitTxTotalCost(localDustLimit, spec, DefaultCommitmentFormat) + val spec = CommitmentSpec(htlcs = Set.empty, commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) + val commitFee = commitTxTotalCost(localDustLimit, spec, ChannelTypes.Standard) val belowDust = (localDustLimit * 0.9).toMilliSatoshi val belowDustWithFee = (localDustLimit + commitFee * 0.9).toMilliSatoshi { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, ChannelTypes.Standard) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToLocal)) assert(outputs.head.output.amount.toMilliSatoshi === toRemoteFundeeBelowDust.toLocal - commitFee) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFee) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, ChannelTypes.Standard) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToRemote)) assert(outputs.head.output.amount.toMilliSatoshi === toLocalFunderBelowDust.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFee) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, ChannelTypes.Standard) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToLocal)) assert(outputs.head.output.amount.toMilliSatoshi === toRemoteFunderBelowDust.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, ChannelTypes.Standard) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToRemote)) assert(outputs.head.output.amount.toMilliSatoshi === toLocalFundeeBelowDust.toRemote - commitFee) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, ChannelTypes.Standard) assert(outputs.isEmpty) } } @@ -275,11 +276,11 @@ class TransactionsSpec extends AnyFunSuite with Logging { OutgoingHtlc(htlc5), IncomingHtlc(htlc6) ), - feeratePerKw = feeratePerKw, + commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.Standard) val commitTxNumber = 0x404142434445L val commitTx = { @@ -297,7 +298,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert((check ^ num) == commitTxNumber) } - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.feeratePerKw, outputs, DefaultCommitmentFormat) + val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ChannelTypes.Standard), outputs, DefaultCommitmentFormat) assert(htlcTxs.length === 4) val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } @@ -430,13 +431,13 @@ class TransactionsSpec extends AnyFunSuite with Logging { } test("generate valid commitment with some outputs that don't materialize (anchor outputs)") { - val spec = CommitmentSpec(htlcs = Set.empty, feeratePerKw = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) - val commitFeeAndAnchorCost = commitTxTotalCost(localDustLimit, spec, AnchorOutputsCommitmentFormat) + val spec = CommitmentSpec(htlcs = Set.empty, commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) + val commitFeeAndAnchorCost = commitTxTotalCost(localDustLimit, spec, ChannelTypes.AnchorOutputs) val belowDust = (localDustLimit * 0.9).toMilliSatoshi val belowDustWithFeeAndAnchors = (localDustLimit + commitFeeAndAnchorCost * 0.9).toMilliSatoshi { - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, AnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.AnchorOutputs) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToRemote, CommitmentOutput.ToLocalAnchor, CommitmentOutput.ToRemoteAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) @@ -445,35 +446,35 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, AnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, ChannelTypes.AnchorOutputs) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFeeAndAnchorCost) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFeeAndAnchors) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, AnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, ChannelTypes.AnchorOutputs) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFeeAndAnchors) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, AnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, ChannelTypes.AnchorOutputs) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, AnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, ChannelTypes.AnchorOutputs) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote - commitFeeAndAnchorCost) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, AnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, ChannelTypes.AnchorOutputs) assert(outputs.isEmpty) } } @@ -496,6 +497,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { // htlc5 and htlc6 are dust IN/OUT htlcs val htlc5 = UpdateAddHtlc(ByteVector32.Zeroes, 5, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(295), TestConstants.emptyOnionPacket) val htlc6 = UpdateAddHtlc(ByteVector32.Zeroes, 6, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(305), TestConstants.emptyOnionPacket) + // htlc7 and htlc8 are at the dust limit when we ignore 2nd-stage tx fees + val htlc7 = UpdateAddHtlc(ByteVector32.Zeroes, 7, localDustLimit.toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(300), TestConstants.emptyOnionPacket) + val htlc8 = UpdateAddHtlc(ByteVector32.Zeroes, 8, localDustLimit.toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(302), TestConstants.emptyOnionPacket) val spec = CommitmentSpec( htlcs = Set( OutgoingHtlc(htlc1), @@ -504,20 +508,23 @@ class TransactionsSpec extends AnyFunSuite with Logging { OutgoingHtlc(htlc3), IncomingHtlc(htlc4), OutgoingHtlc(htlc5), - IncomingHtlc(htlc6) + IncomingHtlc(htlc6), + OutgoingHtlc(htlc7), + IncomingHtlc(htlc8), ), - feeratePerKw = feeratePerKw, + commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) val (commitTx, commitTxOutputs, htlcTimeoutTxs, htlcSuccessTxs) = { val commitTxNumber = 0x404142434445L - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, AnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.AnchorOutputs) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, AnchorOutputsCommitmentFormat) val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.feeratePerKw, outputs, AnchorOutputsCommitmentFormat) + + val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ChannelTypes.AnchorOutputs), outputs, AnchorOutputsCommitmentFormat) assert(htlcTxs.length === 5) val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } @@ -525,6 +532,20 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(htlcTimeoutTxs.map(_.htlcId).toSet === Set(0, 3)) assert(htlcSuccessTxs.size == 3) // htlc2a, htlc2b and htlc4 assert(htlcSuccessTxs.map(_.htlcId).toSet === Set(1, 2, 4)) + + val zeroFeeOutputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + val zeroFeeCommitTx = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, zeroFeeOutputs) + val zeroFeeHtlcTxs = makeHtlcTxs(zeroFeeCommitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ChannelTypes.AnchorOutputsZeroFeeHtlcTx), zeroFeeOutputs, AnchorOutputsCommitmentFormat) + assert(zeroFeeHtlcTxs.length === 7) + val zeroFeeHtlcSuccessTxs = zeroFeeHtlcTxs.collect { case tx: HtlcSuccessTx => tx } + val zeroFeeHtlcTimeoutTxs = zeroFeeHtlcTxs.collect { case tx: HtlcTimeoutTx => tx } + zeroFeeHtlcSuccessTxs.foreach(tx => assert(tx.fee === 0.sat)) + zeroFeeHtlcTimeoutTxs.foreach(tx => assert(tx.fee === 0.sat)) + assert(zeroFeeHtlcTimeoutTxs.size == 3) // htlc1, htlc3 and htlc7 + assert(zeroFeeHtlcTimeoutTxs.map(_.htlcId).toSet === Set(0, 3, 7)) + assert(zeroFeeHtlcSuccessTxs.size == 4) // htlc2a, htlc2b, htlc4 and htlc8 + assert(zeroFeeHtlcSuccessTxs.map(_.htlcId).toSet === Set(1, 2, 4, 8)) + (commitTx, outputs, htlcTimeoutTxs, htlcSuccessTxs) } @@ -742,13 +763,13 @@ class TransactionsSpec extends AnyFunSuite with Logging { OutgoingHtlc(htlc4), OutgoingHtlc(htlc5) ), - feeratePerKw = feeratePerKw, + commitTxFeerate = feeratePerKw, toLocal = millibtc2satoshi(MilliBtc(400)).toMilliSatoshi, toRemote = millibtc2satoshi(MilliBtc(300)).toMilliSatoshi) val commitTxNumber = 0x404142434446L val (commitTx, outputs, htlcTxs) = { - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.Standard) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat) @@ -875,7 +896,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(tests.size === 30, "there were 15 tests at b201efe0546120c14bf154ce5f4e18da7243fe7a") // simple non-reg to make sure we are not missing tests tests.foreach(test => { logger.info(s"running BOLT 3 test: '${test.name}'") - val fee = commitTxTotalCost(dustLimit, test.spec, DefaultCommitmentFormat) + val fee = commitTxTotalCost(dustLimit, test.spec, ChannelTypes.Standard) assert(fee === test.expectedFee) }) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 4c684ec594..6c1f78c742 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -127,11 +127,11 @@ class ChannelCodecsSpec extends AnyFunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"feeratePerKw":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"feeratePerKw":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":1560862173,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":1560862173,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":1561369173,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":1561369173,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"gossip_queries":"optional","option_shutdown_anysegwit":"optional","payment_secret":"optional","option_data_loss_protect":"optional","option_static_remotekey":"optional","basic_mpp":"optional","gossip_queries_ex":"optional","option_support_large_channel":"optional","var_onion_optin":"mandatory"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"gossip_queries":"optional","basic_mpp":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","option_static_remotekey":"mandatory","option_anchors_zero_fee_htlc_tx":"optional","option_upfront_shutdown_script":"optional","option_support_large_channel":"optional","var_onion_optin":"optional"},"unknown":[31]}},"channelFlags":1,"localCommit":{"index":4,"spec":{"htlcs":[],"feeratePerKw":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"feeratePerKw":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":1625746196,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"gossip_queries":"optional","option_shutdown_anysegwit":"optional","payment_secret":"optional","option_data_loss_protect":"optional","option_static_remotekey":"optional","basic_mpp":"optional","gossip_queries_ex":"optional","option_support_large_channel":"optional","var_onion_optin":"mandatory"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"gossip_queries":"optional","basic_mpp":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","option_static_remotekey":"mandatory","option_anchors_zero_fee_htlc_tx":"optional","option_upfront_shutdown_script":"optional","option_support_large_channel":"optional","var_onion_optin":"optional"},"unknown":[31]}},"channelFlags":1,"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":1625746196,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" ) refs.foreach { case (oldbin, refjson) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala index 430239561e..bcd1c32adc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala @@ -139,7 +139,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { assert(setCodec(htlcCodec).decodeValue(setCodec(htlcCodec).encode(htlcs).require).require === htlcs) val o = CommitmentSpec( htlcs = Set(htlc1, htlc2), - feeratePerKw = FeeratePerKw(Random.nextInt(Int.MaxValue).sat), + commitTxFeerate = FeeratePerKw(Random.nextInt(Int.MaxValue).sat), toLocal = MilliSatoshi(Random.nextInt(Int.MaxValue)), toRemote = MilliSatoshi(Random.nextInt(Int.MaxValue)) ) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 946b0a6ef7..2c6a57432a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -169,12 +169,12 @@ class LightningMessageCodecsSpec extends AnyFunSuite { // empty upfront_shutdown_script + default channel type defaultEncoded ++ hex"0000" ++ hex"0100" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard))), // empty upfront_shutdown_script + unsupported channel type - defaultEncoded ++ hex"0000" ++ hex"0103501000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTxs -> FeatureSupport.Mandatory))))), + defaultEncoded ++ hex"0000" ++ hex"0103501000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.UnsupportedChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Mandatory))))), // empty upfront_shutdown_script + channel type defaultEncoded ++ hex"0000" ++ hex"01021000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey))), // non-empty upfront_shutdown_script + channel type defaultEncoded ++ hex"0004 01abcdef" ++ hex"0103101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))), - defaultEncoded ++ hex"0002 abcd" ++ hex"0103401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"abcd"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs))), + defaultEncoded ++ hex"0002 abcd" ++ hex"0103401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"abcd"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))), ) for ((encoded, expected) <- testCases) { diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index f18777be32..ebd060fd57 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -39,12 +39,12 @@ trait Channel { case Some(str) if str == ChannelTypes.Standard.toString => (true, Some(ChannelTypes.Standard)) case Some(str) if str == ChannelTypes.StaticRemoteKey.toString => (true, Some(ChannelTypes.StaticRemoteKey)) case Some(str) if str == ChannelTypes.AnchorOutputs.toString => (true, Some(ChannelTypes.AnchorOutputs)) - case Some(str) if str == ChannelTypes.AnchorOutputsZeroFeeHtlcTxs.toString => (true, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs)) + case Some(str) if str == ChannelTypes.AnchorOutputsZeroFeeHtlcTx.toString => (true, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) case Some(_) => (false, None) case None => (true, None) } if (!channelTypeOk) { - reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be ${ChannelTypes.Standard.toString}, ${ChannelTypes.StaticRemoteKey.toString}, ${ChannelTypes.AnchorOutputs.toString} or ${ChannelTypes.AnchorOutputsZeroFeeHtlcTxs.toString}")) + reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be ${ChannelTypes.Standard.toString}, ${ChannelTypes.StaticRemoteKey.toString}, ${ChannelTypes.AnchorOutputs.toString} or ${ChannelTypes.AnchorOutputsZeroFeeHtlcTx.toString}")) } else { complete { eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, channelFlags, openTimeout_opt) diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index d32a486c56..2391b3766e 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -350,7 +350,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs), None, None, None)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx), None, None, None)(any[Timeout]).wasCalled(once) } } From 80941c2125c62a9dec5bf409c96ad8329fba143e Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 8 Sep 2021 17:14:02 +0200 Subject: [PATCH 3/3] Use separate commitment formats It makes sense to decouple the transaction code that doesn't need to know anything about the channel type, but only the format of the transactions that should be built. --- .../eclair/blockchain/fee/FeeEstimator.scala | 17 +-- .../fr/acinq/eclair/channel/Channel.scala | 14 +-- .../eclair/channel/ChannelFeatures.scala | 6 +- .../fr/acinq/eclair/channel/Commitments.scala | 30 ++--- .../fr/acinq/eclair/channel/Helpers.scala | 54 +++++---- .../publish/ReplaceableTxPublisher.scala | 2 +- .../relay/PostRestartHtlcCleaner.scala | 4 +- .../eclair/transactions/CommitmentSpec.scala | 6 +- .../acinq/eclair/transactions/Scripts.scala | 6 +- .../eclair/transactions/Transactions.scala | 95 +++++++++------ .../eclair/channel/ChannelFeaturesSpec.scala | 4 +- .../fr/acinq/eclair/channel/HelpersSpec.scala | 18 +-- .../ChannelStateTestsHelperMethods.scala | 4 +- .../channel/states/h/ClosingStateSpec.scala | 61 +++++----- .../integration/ChannelIntegrationSpec.scala | 28 +++-- .../transactions/CommitmentSpecSpec.scala | 10 +- .../eclair/transactions/TestVectorsSpec.scala | 10 +- .../transactions/TransactionsSpec.scala | 111 +++++++++--------- 18 files changed, 245 insertions(+), 235 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala index a9d49c5fcb..e7fb5808a9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.blockchain.CurrentFeerates import fr.acinq.eclair.channel.{ChannelTypes, SupportedChannelType} -import fr.acinq.eclair.transactions.Transactions.AnchorOutputsCommitmentFormat +import fr.acinq.eclair.transactions.Transactions trait FeeEstimator { // @formatter:off @@ -40,13 +40,9 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax */ def isFeeDiffTooHigh(channelType: SupportedChannelType, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = { channelType match { - case ChannelTypes.Standard => + case ChannelTypes.Standard | ChannelTypes.StaticRemoteKey => proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate - case ChannelTypes.StaticRemoteKey => - proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate - case ChannelTypes.AnchorOutputs => - proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate - case ChannelTypes.AnchorOutputsZeroFeeHtlcTx => + case ChannelTypes.AnchorOutputs | ChannelTypes.AnchorOutputsZeroFeeHtlcTx => proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate } } @@ -74,10 +70,9 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget) case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget) } - if (channelType.commitmentFormat == AnchorOutputsCommitmentFormat) { - networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate) - } else { - networkFeerate + channelType.commitmentFormat match { + case Transactions.DefaultCommitmentFormat => networkFeerate + case _: Transactions.AnchorOutputsCommitmentFormat => networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index f70858af67..f2549caf95 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -789,8 +789,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val nextCommitNumber = nextRemoteCommit.index // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter - val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.channelType) ++ - Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.channelType) + val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.commitmentFormat) ++ + Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.commitmentFormat) trimmedHtlcs.map(_.add).foreach { htlc => log.info(s"adding paymentHash=${htlc.paymentHash} cltvExpiry=${htlc.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, htlc.paymentHash, htlc.cltvExpiry) @@ -1147,8 +1147,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val nextCommitNumber = nextRemoteCommit.index // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter - val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.channelType) ++ - Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.channelType) + val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.commitmentFormat) ++ + Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec, d.commitments.commitmentFormat) trimmedHtlcs.map(_.add).foreach { htlc => log.info(s"adding paymentHash=${htlc.paymentHash} cltvExpiry=${htlc.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, htlc.paymentHash, htlc.cltvExpiry) @@ -1488,8 +1488,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold val timedOutHtlcs = Closing.isClosingTypeAlreadyKnown(d1) match { - case Some(c: Closing.LocalClose) => Closing.timedOutHtlcs(d.commitments.channelType, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx) - case Some(c: Closing.RemoteClose) => Closing.timedOutHtlcs(d.commitments.channelType, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx) + case Some(c: Closing.LocalClose) => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx) + case Some(c: Closing.RemoteClose) => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx) case _ => Set.empty[UpdateAddHtlc] // we lose htlc outputs in dataloss protection scenarii (future remote commit) } timedOutHtlcs.foreach { add => @@ -2381,7 +2381,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Transactions.DefaultCommitmentFormat => val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishRawTx(tx, Some(commitTx.txid))) List(PublishRawTx(commitTx, commitInput, "commit-tx", None)) ++ (claimMainDelayedOutputTx.map(tx => PublishRawTx(tx, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishRawTx(tx, None))) - case Transactions.AnchorOutputsCommitmentFormat => + case _: Transactions.AnchorOutputsCommitmentFormat => val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => PublishReplaceableTx(tx, commitments) } val redeemableHtlcTxs = htlcTxs.values.collect { case Some(tx) => PublishReplaceableTx(tx, commitments) } List(PublishRawTx(commitTx, commitInput, "commit-tx", None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishRawTx(tx, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishRawTx(tx, None)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 373d9ee7b2..54bb0a7e11 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} import fr.acinq.eclair.{Feature, FeatureSupport, Features} /** @@ -98,13 +98,13 @@ object ChannelTypes { case object AnchorOutputs extends SupportedChannelType { override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) override def paysDirectlyToWallet: Boolean = false - override def commitmentFormat: CommitmentFormat = AnchorOutputsCommitmentFormat + override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs" } case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType { override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) override def paysDirectlyToWallet: Boolean = false - override def commitmentFormat: CommitmentFormat = AnchorOutputsCommitmentFormat + override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs_zero_fee_htlc_tx" } case class UnsupportedChannelType(featureBits: Features) extends ChannelType { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index dc79f63076..ee3c039950 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -242,11 +242,11 @@ case class Commitments(channelId: ByteVector32, val balanceNoFees = (reduced.toRemote - remoteParams.channelReserve).max(0 msat) if (localParams.isFunder) { // The funder always pays the on-chain fees, so we must subtract that from the amount we can send. - val commitFees = commitTxTotalCostMsat(remoteParams.dustLimit, reduced, channelType) + val commitFees = commitTxTotalCostMsat(remoteParams.dustLimit, reduced, commitmentFormat) // the funder needs to keep a "funder fee buffer" (see explanation above) - val funderFeeBuffer = commitTxTotalCostMsat(remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), channelType) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) + val funderFeeBuffer = commitTxTotalCostMsat(remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve = commitFees.max(funderFeeBuffer) - if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced, channelType)) { + if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced, commitmentFormat)) { // htlc will be trimmed (balanceNoFees - amountToReserve).max(0 msat) } else { @@ -271,11 +271,11 @@ case class Commitments(channelId: ByteVector32, balanceNoFees } else { // The funder always pays the on-chain fees, so we must subtract that from the amount we can receive. - val commitFees = commitTxTotalCostMsat(localParams.dustLimit, reduced, channelType) + val commitFees = commitTxTotalCostMsat(localParams.dustLimit, reduced, commitmentFormat) // we expected the funder to keep a "funder fee buffer" (see explanation above) - val funderFeeBuffer = commitTxTotalCostMsat(localParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), channelType) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) + val funderFeeBuffer = commitTxTotalCostMsat(localParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve = commitFees.max(funderFeeBuffer) - if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localParams.dustLimit, reduced, channelType)) { + if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localParams.dustLimit, reduced, commitmentFormat)) { // htlc will be trimmed (balanceNoFees - amountToReserve).max(0 msat) } else { @@ -357,10 +357,10 @@ object Commitments { val outgoingHtlcs = reduced.htlcs.collect(incoming) // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment - val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.channelType) + val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) // the funder needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid // getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728). - val funderFeeBuffer = commitTxTotalCostMsat(commitments1.remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitments.channelType) + htlcOutputFee(reduced.commitTxFeerate * 2, commitments.commitmentFormat) + val funderFeeBuffer = commitTxTotalCostMsat(commitments1.remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitments.commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitments.commitmentFormat) // NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold) // which may result in a lower commit tx fee; this is why we take the max of the two. val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) @@ -416,7 +416,7 @@ object Commitments { val incomingHtlcs = reduced.htlcs.collect(incoming) // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment - val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.channelType) + val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) // NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote funder that doesn't have this mitigation in place // We could enforce it once we're confident a large portion of the network implements it. val missingForSender = reduced.toRemote - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees) @@ -535,7 +535,7 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is funder remote doesn't pay the fees - val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.channelType) + val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteParams.channelReserve - fees if (missing < 0.sat) { Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees)) @@ -568,7 +568,7 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee - val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.channelType) + val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) val missing = reduced.toRemote.truncateToSatoshi - commitments1.localParams.channelReserve - fees if (missing < 0.sat) { Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees)) @@ -754,9 +754,9 @@ object Commitments { val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelFeatures.channelType) + val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelFeatures.commitmentFormat) val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isFunder, outputs) - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.channelType), outputs, channelFeatures.commitmentFormat) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.commitmentFormat), outputs, channelFeatures.commitmentFormat) (commitTx, htlcTxs) } @@ -781,9 +781,9 @@ object Commitments { val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, spec, channelFeatures.channelType) + val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, spec, channelFeatures.commitmentFormat) val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentBasepoint, !localParams.isFunder, outputs) - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.channelType), outputs, channelFeatures.commitmentFormat) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.commitmentFormat), outputs, channelFeatures.commitmentFormat) (commitTx, htlcTxs) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 619d4439ad..9c6c84a594 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -295,7 +295,7 @@ object Helpers { if (!localParams.isFunder) { // they are funder, therefore they pay the fee: we need to make sure they can afford it! val toRemoteMsat = remoteSpec.toLocal - val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.channelType) + val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat) val missing = toRemoteMsat.truncateToSatoshi - localParams.channelReserve - fees if (missing < Satoshi(0)) { return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.channelReserve, fees = fees)) @@ -472,11 +472,11 @@ object Helpers { def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): ClosingFees = { val requestedFeerate = feeEstimator.getFeeratePerKw(feeTargets.mutualCloseBlockTarget) - val preferredFeerate = if (commitments.commitmentFormat == AnchorOutputsCommitmentFormat) { - requestedFeerate - } else { - // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" - requestedFeerate.min(commitments.localCommit.spec.commitTxFeerate) + val preferredFeerate = commitments.commitmentFormat match { + case DefaultCommitmentFormat => + // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" + requestedFeerate.min(commitments.localCommit.spec.commitTxFeerate) + case _: AnchorOutputsCommitmentFormat => requestedFeerate } // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can // always use CPFP to speed up confirmation if necessary. @@ -514,7 +514,7 @@ object Helpers { def checkClosingSignature(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = { import commitments._ val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount - commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.map(_.amount).sum - if (remoteClosingFee > lastCommitFeeSatoshi && commitments.commitmentFormat != AnchorOutputsCommitmentFormat) { + if (remoteClosingFee > lastCommitFeeSatoshi && commitments.commitmentFormat == DefaultCommitmentFormat) { log.error(s"remote proposed a commit fee higher than the last commitment fee: remote closing fee=${remoteClosingFee.toLong} last commit fees=$lastCommitFeeSatoshi") Left(InvalidCloseFee(commitments.channelId, remoteClosingFee)) } else { @@ -658,7 +658,7 @@ object Helpers { val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, remoteCommit.spec, commitments.channelType) + val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, remoteCommit.spec, commitments.commitmentFormat) // we need to use a rather high fee for htlc-claim because we compete with the counterparty val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) @@ -744,7 +744,7 @@ object Helpers { Transactions.addSigs(claimMain, localPubkey, sig) }) } - case AnchorOutputsCommitmentFormat => generateTx("remote-main-delayed") { + case _: AnchorOutputsCommitmentFormat => generateTx("remote-main-delayed") { Transactions.makeClaimRemoteDelayedOutputTx(tx, commitments.localParams.dustLimit, localPaymentPoint, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitments.commitmentFormat) Transactions.addSigs(claimMain, sig) @@ -800,17 +800,19 @@ object Helpers { case ct if ct.paysDirectlyToWallet => log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do") None - case ct if ct.commitmentFormat == AnchorOutputsCommitmentFormat => generateTx("remote-main-delayed") { - Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) - Transactions.addSigs(claimMain, sig) - }) - } - case _ => generateTx("claim-p2wpkh-output") { - Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) - Transactions.addSigs(claimMain, localPaymentPubkey, sig) - }) + case ct => ct.commitmentFormat match { + case DefaultCommitmentFormat => generateTx("claim-p2wpkh-output") { + Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimMain, localPaymentPubkey, sig) + }) + } + case _: AnchorOutputsCommitmentFormat => generateTx("remote-main-delayed") { + Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimMain, sig) + }) + } } } @@ -1003,15 +1005,15 @@ object Helpers { * @param tx a tx that has reached mindepth * @return a set of htlcs that need to be failed upstream */ - def timedOutHtlcs(channelType: SupportedChannelType, localCommit: LocalCommit, localCommitPublished: LocalCommitPublished, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { - val untrimmedHtlcs = Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec, channelType).map(_.add) + def timedOutHtlcs(commitmentFormat: CommitmentFormat, localCommit: LocalCommit, localCommitPublished: LocalCommitPublished, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { + val untrimmedHtlcs = Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec, commitmentFormat).map(_.add) if (tx.txid == localCommit.commitTxAndRemoteSig.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) localCommit.spec.htlcs.collect(outgoing) -- untrimmedHtlcs } else { // maybe this is a timeout tx, in that case we can resolve and fail the corresponding htlc val isMissingHtlcIndex = localCommitPublished.htlcTxs.values.collect { case Some(HtlcTimeoutTx(_, _, htlcId)) => htlcId }.toSet == Set(0) - if (isMissingHtlcIndex && channelType.commitmentFormat == DefaultCommitmentFormat) { + if (isMissingHtlcIndex && commitmentFormat == DefaultCommitmentFormat) { tx.txIn .map(_.witness) .collect(Scripts.extractPaymentHashFromHtlcTimeout) @@ -1044,15 +1046,15 @@ object Helpers { * @param tx a tx that has reached mindepth * @return a set of htlcs that need to be failed upstream */ - def timedOutHtlcs(channelType: SupportedChannelType, remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished, remoteDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { - val untrimmedHtlcs = Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec, channelType).map(_.add) + def timedOutHtlcs(commitmentFormat: CommitmentFormat, remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished, remoteDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { + val untrimmedHtlcs = Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec, commitmentFormat).map(_.add) if (tx.txid == remoteCommit.txid) { // the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx) remoteCommit.spec.htlcs.collect(incoming) -- untrimmedHtlcs } else { // maybe this is a timeout tx, in that case we can resolve and fail the corresponding htlc val isMissingHtlcIndex = remoteCommitPublished.claimHtlcTxs.values.collect { case Some(ClaimHtlcTimeoutTx(_, _, htlcId)) => htlcId }.toSet == Set(0) - if (isMissingHtlcIndex && channelType.commitmentFormat == DefaultCommitmentFormat) { + if (isMissingHtlcIndex && commitmentFormat == DefaultCommitmentFormat) { tx.txIn .map(_.witness) .collect(Scripts.extractPaymentHashFromClaimHtlcTimeout) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala index 481de0ebd0..9170fce2f6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala @@ -239,7 +239,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, } def checkHtlcPreconditions(replyTo: ActorRef[TxPublisher.PublishTxResult], cmd: TxPublisher.PublishReplaceableTx, htlcTx: HtlcTx, targetFeerate: FeeratePerKw): Behavior[Command] = { - val htlcFeerate = cmd.commitments.localCommit.spec.htlcTxFeerate(cmd.commitments.channelType) + val htlcFeerate = cmd.commitments.localCommit.spec.htlcTxFeerate(cmd.commitments.commitmentFormat) // HTLC transactions have a 1-block relative delay when using anchor outputs, which ensures our commit tx has already // been confirmed (we don't need to check again here). HtlcTxAndWitnessData(htlcTx, cmd.commitments) match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index abe1844d1f..79dcb2c3e5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -372,10 +372,10 @@ object PostRestartHtlcCleaner { val timedOutHtlcs: Set[Long] = (closingType_opt match { case Some(c: Closing.LocalClose) => val confirmedTxs = c.localCommitPublished.commitTx +: irrevocablySpent.filter(tx => Closing.isHtlcTimeout(tx, c.localCommitPublished)) - confirmedTxs.flatMap(tx => Closing.timedOutHtlcs(d.commitments.channelType, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx)) + confirmedTxs.flatMap(tx => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx)) case Some(c: Closing.RemoteClose) => val confirmedTxs = c.remoteCommitPublished.commitTx +: irrevocablySpent.filter(tx => Closing.isClaimHtlcTimeout(tx, c.remoteCommitPublished)) - confirmedTxs.flatMap(tx => Closing.timedOutHtlcs(d.commitments.channelType, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx)) + confirmedTxs.flatMap(tx => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx)) case _ => Seq.empty[UpdateAddHtlc] }).map(_.id).toSet overriddenHtlcs ++ timedOutHtlcs diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index 8d7f5cb49d..20113214a4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.SatoshiLong import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.{ChannelTypes, SupportedChannelType} +import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} import fr.acinq.eclair.wire.protocol._ /** @@ -74,8 +74,8 @@ case class OutgoingHtlc(add: UpdateAddHtlc) extends DirectedHtlc final case class CommitmentSpec(htlcs: Set[DirectedHtlc], commitTxFeerate: FeeratePerKw, toLocal: MilliSatoshi, toRemote: MilliSatoshi) { - def htlcTxFeerate(channelType: SupportedChannelType): FeeratePerKw = channelType match { - case ChannelTypes.AnchorOutputsZeroFeeHtlcTx => FeeratePerKw(0 sat) + def htlcTxFeerate(commitmentFormat: CommitmentFormat): FeeratePerKw = commitmentFormat match { + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat => FeeratePerKw(0 sat) case _ => commitTxFeerate } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala index 1e2585e900..5c8b93dffe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala @@ -38,7 +38,7 @@ object Scripts { private def htlcRemoteSighash(commitmentFormat: CommitmentFormat): Int = commitmentFormat match { case DefaultCommitmentFormat => SIGHASH_ALL - case AnchorOutputsCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY + case _: AnchorOutputsCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY } def multiSig2of2(pubkey1: PublicKey, pubkey2: PublicKey): Seq[ScriptElt] = @@ -186,7 +186,7 @@ object Scripts { def htlcOffered(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, revocationPubKey: PublicKey, paymentHash: ByteVector, commitmentFormat: CommitmentFormat): Seq[ScriptElt] = { val addCsvDelay = commitmentFormat match { case DefaultCommitmentFormat => false - case AnchorOutputsCommitmentFormat => true + case _: AnchorOutputsCommitmentFormat => true } // @formatter:off // To you with revocation key @@ -237,7 +237,7 @@ object Scripts { def htlcReceived(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, revocationPubKey: PublicKey, paymentHash: ByteVector, lockTime: CltvExpiry, commitmentFormat: CommitmentFormat): Seq[ScriptElt] = { val addCsvDelay = commitmentFormat match { case DefaultCommitmentFormat => false - case AnchorOutputsCommitmentFormat => true + case _: AnchorOutputsCommitmentFormat => true } // @formatter:off // To you with revocation key 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 26f09b51ea..51ae6cb6a6 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 @@ -22,7 +22,6 @@ import fr.acinq.bitcoin.SigVersion._ import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.SupportedChannelType import fr.acinq.eclair.transactions.CommitmentOutput._ import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc @@ -59,14 +58,30 @@ object Transactions { * Commitment format that adds anchor outputs to the commitment transaction and uses custom sighash flags for HTLC * transactions to allow unilateral fee bumping (https://github.com/lightningnetwork/lightning-rfc/pull/688). */ - case object AnchorOutputsCommitmentFormat extends CommitmentFormat { - val anchorAmount = Satoshi(330) + sealed trait AnchorOutputsCommitmentFormat extends CommitmentFormat { override val commitWeight = 1124 override val htlcOutputWeight = 172 override val htlcTimeoutWeight = 666 override val htlcSuccessWeight = 706 } + object AnchorOutputsCommitmentFormat { + val anchorAmount = Satoshi(330) + } + + /** + * This commitment format may be unsafe where you're fundee, as it exposes you to a fee inflating attack. + * Don't use this commitment format unless you know what you're doing! + * See https://lists.linuxfoundation.org/pipermail/lightning-dev/2020-September/002796.html for details. + */ + case object UnsafeLegacyAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat + + /** + * This commitment format removes the fees from the pre-signed 2nd-stage htlc transactions to fix the fee inflating + * attack against [[UnsafeLegacyAnchorOutputsCommitmentFormat]]. + */ + case object ZeroFeeHtlcTxAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat + // @formatter:off case class OutputInfo(index: Long, amount: Satoshi, publicKeyScript: ByteVector) case class InputInfo(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector) @@ -100,7 +115,7 @@ object Transactions { def htlcId: Long override def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = commitmentFormat match { case DefaultCommitmentFormat => SIGHASH_ALL - case AnchorOutputsCommitmentFormat => txOwner match { + case _: AnchorOutputsCommitmentFormat => txOwner match { case TxOwner.Local => SIGHASH_ALL case TxOwner.Remote => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY } @@ -188,22 +203,22 @@ object Transactions { def fee2rate(fee: Satoshi, weight: Int): FeeratePerKw = FeeratePerKw((fee * 1000L) / weight) /** Offered HTLCs below this amount will be trimmed. */ - def offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): Satoshi = - dustLimit + weight2fee(spec.htlcTxFeerate(channelType), channelType.commitmentFormat.htlcTimeoutWeight) + def offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = + dustLimit + weight2fee(spec.htlcTxFeerate(commitmentFormat), commitmentFormat.htlcTimeoutWeight) - def trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): Seq[OutgoingHtlc] = { - val threshold = offeredHtlcTrimThreshold(dustLimit, spec, channelType) + def trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Seq[OutgoingHtlc] = { + val threshold = offeredHtlcTrimThreshold(dustLimit, spec, commitmentFormat) spec.htlcs .collect { case o: OutgoingHtlc if o.add.amountMsat >= threshold => o } .toSeq } /** Received HTLCs below this amount will be trimmed. */ - def receivedHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): Satoshi = - dustLimit + weight2fee(spec.htlcTxFeerate(channelType), channelType.commitmentFormat.htlcSuccessWeight) + def receivedHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = + dustLimit + weight2fee(spec.htlcTxFeerate(commitmentFormat), commitmentFormat.htlcSuccessWeight) - def trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): Seq[IncomingHtlc] = { - val threshold = receivedHtlcTrimThreshold(dustLimit, spec, channelType) + def trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Seq[IncomingHtlc] = { + val threshold = receivedHtlcTrimThreshold(dustLimit, spec, commitmentFormat) spec.htlcs .collect { case i: IncomingHtlc if i.add.amountMsat >= threshold => i } .toSeq @@ -213,10 +228,10 @@ object Transactions { def htlcOutputFee(feeratePerKw: FeeratePerKw, commitmentFormat: CommitmentFormat): MilliSatoshi = weight2feeMsat(feeratePerKw, commitmentFormat.htlcOutputWeight) /** Fee paid by the commit tx (depends on which HTLCs will be trimmed). */ - def commitTxFeeMsat(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): MilliSatoshi = { - val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec, channelType) - val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec, channelType) - val weight = channelType.commitmentFormat.commitWeight + channelType.commitmentFormat.htlcOutputWeight * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) + def commitTxFeeMsat(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): MilliSatoshi = { + val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec, commitmentFormat) + val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec, commitmentFormat) + val weight = commitmentFormat.commitWeight + commitmentFormat.htlcOutputWeight * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) weight2feeMsat(spec.commitTxFeerate, weight) } @@ -226,19 +241,19 @@ object Transactions { * If you are adding multiple fees together for example, you should always add them in MilliSatoshi and then round * down to Satoshi. */ - def commitTxTotalCostMsat(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): MilliSatoshi = { + def commitTxTotalCostMsat(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): MilliSatoshi = { // The funder pays the on-chain fee by deducing it from its main output. - val txFee = commitTxFeeMsat(dustLimit, spec, channelType) + val txFee = commitTxFeeMsat(dustLimit, spec, commitmentFormat) // When using anchor outputs, the funder pays for *both* anchors all the time, even if only one anchor is present. // This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the funder's main output. - val anchorsCost = channelType.commitmentFormat match { + val anchorsCost = commitmentFormat match { case DefaultCommitmentFormat => Satoshi(0) - case AnchorOutputsCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 + case _: AnchorOutputsCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 } txFee + anchorsCost } - def commitTxTotalCost(dustLimit: Satoshi, spec: CommitmentSpec, channelType: SupportedChannelType): Satoshi = commitTxTotalCostMsat(dustLimit, spec, channelType).truncateToSatoshi + def commitTxTotalCost(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = commitTxTotalCostMsat(dustLimit, spec, commitmentFormat).truncateToSatoshi /** * @param commitTxNumber commit tx number @@ -287,7 +302,7 @@ object Transactions { def getHtlcTxInputSequence(commitmentFormat: CommitmentFormat): Long = commitmentFormat match { case DefaultCommitmentFormat => 0 // htlc txs immediately spend the commit tx - case AnchorOutputsCommitmentFormat => 1 // htlc txs have a 1-block delay to allow CPFP carve-out on anchors + case _: AnchorOutputsCommitmentFormat => 1 // htlc txs have a 1-block delay to allow CPFP carve-out on anchors } /** @@ -326,25 +341,25 @@ object Transactions { localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, spec: CommitmentSpec, - channelType: SupportedChannelType): CommitmentOutputs = { + commitmentFormat: CommitmentFormat): CommitmentOutputs = { val outputs = collection.mutable.ArrayBuffer.empty[CommitmentOutputLink[CommitmentOutput]] - trimOfferedHtlcs(localDustLimit, spec, channelType).foreach { htlc => - val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), channelType.commitmentFormat) + trimOfferedHtlcs(localDustLimit, spec, commitmentFormat).foreach { htlc => + val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), commitmentFormat) outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, OutHtlc(htlc))) } - trimReceivedHtlcs(localDustLimit, spec, channelType).foreach { htlc => - val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry, channelType.commitmentFormat) + trimReceivedHtlcs(localDustLimit, spec, commitmentFormat).foreach { htlc => + val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry, commitmentFormat) outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) } val hasHtlcs = outputs.nonEmpty val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { - (spec.toLocal.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, channelType), spec.toRemote.truncateToSatoshi) + (spec.toLocal.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat), spec.toRemote.truncateToSatoshi) } else { - (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, channelType)) + (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat)) } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway if (toLocalAmount >= localDustLimit) { @@ -355,25 +370,27 @@ object Transactions { } if (toRemoteAmount >= localDustLimit) { - channelType.commitmentFormat match { + commitmentFormat match { case DefaultCommitmentFormat => outputs.append(CommitmentOutputLink( TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey)), pay2pkh(remotePaymentPubkey), ToRemote)) - case AnchorOutputsCommitmentFormat => outputs.append(CommitmentOutputLink( + case _: AnchorOutputsCommitmentFormat => outputs.append(CommitmentOutputLink( TxOut(toRemoteAmount, pay2wsh(toRemoteDelayed(remotePaymentPubkey))), toRemoteDelayed(remotePaymentPubkey), ToRemote)) } } - if (channelType.commitmentFormat == AnchorOutputsCommitmentFormat) { - if (toLocalAmount >= localDustLimit || hasHtlcs) { - outputs.append(CommitmentOutputLink(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2wsh(anchor(localFundingPubkey))), anchor(localFundingPubkey), ToLocalAnchor)) - } - if (toRemoteAmount >= localDustLimit || hasHtlcs) { - outputs.append(CommitmentOutputLink(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2wsh(anchor(remoteFundingPubkey))), anchor(remoteFundingPubkey), ToRemoteAnchor)) - } + commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => + if (toLocalAmount >= localDustLimit || hasHtlcs) { + outputs.append(CommitmentOutputLink(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2wsh(anchor(localFundingPubkey))), anchor(localFundingPubkey), ToLocalAnchor)) + } + if (toRemoteAmount >= localDustLimit || hasHtlcs) { + outputs.append(CommitmentOutputLink(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2wsh(anchor(remoteFundingPubkey))), anchor(remoteFundingPubkey), ToRemoteAnchor)) + } + case _ => } outputs.sortWith(CommitmentOutputLink.sort).toSeq @@ -490,7 +507,7 @@ object Transactions { val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) val sequence = commitmentFormat match { case DefaultCommitmentFormat => 0xffffffffL // RBF disabled - case AnchorOutputsCommitmentFormat => 1 // txs have a 1-block delay to allow CPFP carve-out on anchors + case _: AnchorOutputsCommitmentFormat => 1 // txs have a 1-block delay to allow CPFP carve-out on anchors } val tx = Transaction( version = 2, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index 6741c4f864..e45beec1e1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -44,13 +44,13 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha assert(anchorOutputsChannel.hasFeature(Features.StaticRemoteKey)) assert(anchorOutputsChannel.hasFeature(Features.AnchorOutputs)) assert(!anchorOutputsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) - assert(anchorOutputsChannel.commitmentFormat === Transactions.AnchorOutputsCommitmentFormat) + assert(anchorOutputsChannel.commitmentFormat === Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) assert(!anchorOutputsChannel.paysDirectlyToWallet) assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.StaticRemoteKey)) assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) assert(!anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputs)) - assert(anchorOutputsZeroFeeHtlcsChannel.commitmentFormat === Transactions.AnchorOutputsCommitmentFormat) + assert(anchorOutputsZeroFeeHtlcsChannel.commitmentFormat === Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) assert(!anchorOutputsZeroFeeHtlcsChannel.paysDirectlyToWallet) } 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 33201cd61b..ce1590c6ae 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 @@ -174,7 +174,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat import f._ val dustLimit = alice.underlyingActor.nodeParams.dustLimit - val channelType = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelType + val commitmentFormat = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat val localCommit = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.localCommit val remoteCommit = bob.stateData.asInstanceOf[DATA_CLOSING].commitments.remoteCommit @@ -189,25 +189,25 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(remoteCommitPublished) val aliceTimedOutHtlcs = htlcTimeoutTxs.map(htlcTimeout => { - val timedOutHtlcs = Closing.timedOutHtlcs(channelType, localCommit, localCommitPublished, dustLimit, htlcTimeout.tx) + val timedOutHtlcs = Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcTimeout.tx) assert(timedOutHtlcs.size === 1) timedOutHtlcs.head }) assert(aliceTimedOutHtlcs.toSet === aliceHtlcs) val bobTimedOutHtlcs = claimHtlcTimeoutTxs.map(claimHtlcTimeout => { - val timedOutHtlcs = Closing.timedOutHtlcs(channelType, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcTimeout.tx) + val timedOutHtlcs = Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcTimeout.tx) assert(timedOutHtlcs.size === 1) timedOutHtlcs.head }) assert(bobTimedOutHtlcs.toSet === bobHtlcs) - htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.timedOutHtlcs(channelType, localCommit, localCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) - htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.timedOutHtlcs(channelType, remoteCommit, remoteCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.timedOutHtlcs(channelType, localCommit, localCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.timedOutHtlcs(channelType, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) - htlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.timedOutHtlcs(channelType, remoteCommit, remoteCommitPublished, dustLimit, htlcTimeout.tx).isEmpty)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.timedOutHtlcs(channelType, localCommit, localCommitPublished, dustLimit, claimHtlcTimeout.tx).isEmpty)) + htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) + htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) + claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) + claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) + htlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, htlcTimeout.tx).isEmpty)) + claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, claimHtlcTimeout.tx).isEmpty)) } test("find timed out htlcs") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index c317d5cdd5..1c6310ff8f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -331,7 +331,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { assert(publishedLocalCommitTx.txid == commitTx.txid) val commitInput = closingState.commitments.commitInput Transaction.correctlySpends(publishedLocalCommitTx, Map(commitInput.outPoint -> commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - if (closingState.commitments.commitmentFormat == Transactions.AnchorOutputsCommitmentFormat) { + if (closingState.commitments.commitmentFormat.isInstanceOf[Transactions.AnchorOutputsCommitmentFormat]) { assert(s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) } // if s has a main output in the commit tx (when it has a non-dust balance), it should be claimed @@ -341,7 +341,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { // all htlcs success/timeout should be published as-is, without claiming their outputs s2blockchain.expectMsgAllOf(localCommitPublished.htlcTxs.values.toSeq.collect { case Some(tx) => TxPublisher.PublishRawTx(tx, Some(commitTx.txid)) }: _*) assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) - case Transactions.AnchorOutputsCommitmentFormat => + case _: Transactions.AnchorOutputsCommitmentFormat => // all htlcs success/timeout should be published as replaceable txs, without claiming their outputs val htlcTxs = localCommitPublished.htlcTxs.values.collect { case Some(tx: HtlcTx) => tx } val publishedTxs = htlcTxs.map(_ => s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 85384b44ef..f712645191 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -22,13 +22,12 @@ import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLo import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} -import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.publish.TxPublisher.{PublishRawTx, PublishTx, SetChannelId} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer._ -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, HtlcSuccessTx, HtlcTimeoutTx} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, HtlcSuccessTx, HtlcTimeoutTx, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey} @@ -701,10 +700,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the latest commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { - assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs - } else { - assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs + channelFeatures.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs + case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimHtlcTxs.size === 3) @@ -833,10 +831,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the next commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { - assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs - } else { - assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs + channelFeatures.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs + case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) assert(getClaimHtlcTimeoutTxs(closingState).length === 3) @@ -1013,10 +1010,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) // bob is nice and publishes its commitment val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { - assert(bobCommitTx.txOut.length === 6) // two main outputs + two anchors + 2 HTLCs - } else { - assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs + channelFeatures.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length === 6) // two main outputs + two anchors + 2 HTLCs + case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs } alice ! WatchFundingSpentTriggered(bobCommitTx) bobCommitTx @@ -1091,10 +1087,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob's first commit tx doesn't contain any htlc val localCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit - if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { - assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) // 2 main outputs + 2 anchors - } else { - assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) // 2 main outputs + channelFeatures.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) // 2 main outputs + 2 anchors + case DefaultCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) // 2 main outputs } // Bob's second commit tx contains 1 incoming htlc and 1 outgoing htlc @@ -1108,10 +1103,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size) - if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { - assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) - } else { - assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) + channelFeatures.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) + case DefaultCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) } // Bob's third commit tx contains 2 incoming htlcs and 2 outgoing htlcs @@ -1125,10 +1119,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size) - if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { - assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8) - } else { - assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) + channelFeatures.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8) + case DefaultCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) } // Bob's fourth commit tx doesn't contain any htlc @@ -1140,10 +1133,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size) - if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { - assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) - } else { - assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) + channelFeatures.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) + case DefaultCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) } RevokedCloseFixture(Seq(localCommit1, localCommit2, localCommit3, localCommit4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2)) @@ -1184,7 +1176,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice spends all outpoints of the revoked tx, except her main output when it goes directly to our wallet val spentOutpoints = penaltyTxs.flatMap(_.tx.txIn.map(_.outPoint)).toSet assert(spentOutpoints.forall(_.txid === bobRevokedTx.txid)) - if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) { + if (channelFeatures.commitmentFormat.isInstanceOf[AnchorOutputsCommitmentFormat]) { assert(spentOutpoints.size === bobRevokedTx.txOut.size - 2) // we don't claim the anchors } else if (channelFeatures.paysDirectlyToWallet) { @@ -1454,7 +1446,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val commitmentFormat = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitmentFormat alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat === AnchorOutputsCommitmentFormat) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat === ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head assert(rvk.commitTx === bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) @@ -1526,7 +1518,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with private def testRevokedTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) - val initOutputCount = if (channelFeatures.commitmentFormat == AnchorOutputsCommitmentFormat) 4 else 2 + val initOutputCount = channelFeatures.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat => 4 + case DefaultCommitmentFormat => 2 + } assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size === initOutputCount) // bob's second commit tx contains 2 incoming htlcs diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 5f106b175e..d7fd7b1053 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.router.Router -import fr.acinq.eclair.transactions.Transactions.TxOwner +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, TxOwner} import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, PermanentChannelFailure, UpdateAddHtlc} import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, randomBytes32} @@ -397,7 +397,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val localCommitF = commitmentsF.localCommit commitmentFormat match { case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) - case Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8) + case _: Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8) } val htlcTimeoutTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcTimeoutTx, _) => h } val htlcSuccessTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcSuccessTx, _) => h } @@ -631,6 +631,8 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { + val commitmentFormat: AnchorOutputsCommitmentFormat + def connectNodes(expectedChannelType: SupportedChannelType): Unit = { // A --- C --- F val eventListener = TestProbe() @@ -746,7 +748,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { } def testPunishRevokedCommit(): Unit = { - val revokedCommitFixture = testRevokedCommit(Transactions.AnchorOutputsCommitmentFormat) + val revokedCommitFixture = testRevokedCommit(commitmentFormat) import revokedCommitFixture._ val bitcoinClient = new ExtendedBitcoinClient(bitcoinrpcclient) @@ -778,6 +780,8 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { class AnchorOutputChannelIntegrationSpec extends AnchorChannelIntegrationSpec { + override val commitmentFormat = Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat + test("start eclair nodes") { instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29750, "eclair.api.port" -> 28093).asJava).withFallback(commonFeatures).withFallback(commonConfig)) instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29751, "eclair.api.port" -> 28094).asJava).withFallback(withAnchorOutputs).withFallback(withWumbo).withFallback(commonConfig)) @@ -793,19 +797,19 @@ class AnchorOutputChannelIntegrationSpec extends AnchorChannelIntegrationSpec { } test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs)") { - testDownstreamFulfillLocalCommit(Transactions.AnchorOutputsCommitmentFormat) + testDownstreamFulfillLocalCommit(commitmentFormat) } test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs)") { - testDownstreamFulfillRemoteCommit(Transactions.AnchorOutputsCommitmentFormat) + testDownstreamFulfillRemoteCommit(commitmentFormat) } test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs)") { - testDownstreamTimeoutLocalCommit(Transactions.AnchorOutputsCommitmentFormat) + testDownstreamTimeoutLocalCommit(commitmentFormat) } test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs)") { - testDownstreamTimeoutRemoteCommit(Transactions.AnchorOutputsCommitmentFormat) + testDownstreamTimeoutRemoteCommit(commitmentFormat) } test("punish a node that has published a revoked commit tx (anchor outputs)") { @@ -816,6 +820,8 @@ class AnchorOutputChannelIntegrationSpec extends AnchorChannelIntegrationSpec { class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelIntegrationSpec { + override val commitmentFormat = Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat + test("start eclair nodes") { instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29760, "eclair.api.port" -> 28096).asJava).withFallback(commonFeatures).withFallback(commonConfig)) instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29761, "eclair.api.port" -> 28097).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(withWumbo).withFallback(commonConfig)) @@ -831,19 +837,19 @@ class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelInte } test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs zero fee htlc txs)") { - testDownstreamFulfillLocalCommit(Transactions.AnchorOutputsCommitmentFormat) + testDownstreamFulfillLocalCommit(commitmentFormat) } test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs zero fee htlc txs)") { - testDownstreamFulfillRemoteCommit(Transactions.AnchorOutputsCommitmentFormat) + testDownstreamFulfillRemoteCommit(commitmentFormat) } test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs zero fee htlc txs)") { - testDownstreamTimeoutLocalCommit(Transactions.AnchorOutputsCommitmentFormat) + testDownstreamTimeoutLocalCommit(commitmentFormat) } test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs zero fee htlc txs)") { - testDownstreamTimeoutRemoteCommit(Transactions.AnchorOutputsCommitmentFormat) + testDownstreamTimeoutRemoteCommit(commitmentFormat) } test("punish a node that has published a revoked commit tx (anchor outputs)") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala index 6e43d76b4e..a144195afb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala @@ -18,7 +18,6 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.{ByteVector32, Crypto, SatoshiLong} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.ChannelTypes import fr.acinq.eclair.wire.protocol.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong, TestConstants, randomBytes32} import org.scalatest.funsuite.AnyFunSuite @@ -69,12 +68,11 @@ class CommitmentSpecSpec extends AnyFunSuite { assert(spec4 === spec3.copy(htlcs = Set(), toRemote = (3000 * 1000) msat)) } - test("compute htlc tx feerate based on channel type") { + test("compute htlc tx feerate based on commitment format") { val spec = CommitmentSpec(htlcs = Set(), commitTxFeerate = FeeratePerKw(2500 sat), toLocal = (5000 * 1000) msat, toRemote = (2500 * 1000) msat) - assert(spec.htlcTxFeerate(ChannelTypes.Standard) === FeeratePerKw(2500 sat)) - assert(spec.htlcTxFeerate(ChannelTypes.StaticRemoteKey) === FeeratePerKw(2500 sat)) - assert(spec.htlcTxFeerate(ChannelTypes.AnchorOutputs) === FeeratePerKw(2500 sat)) - assert(spec.htlcTxFeerate(ChannelTypes.AnchorOutputsZeroFeeHtlcTx) === FeeratePerKw(0 sat)) + assert(spec.htlcTxFeerate(Transactions.DefaultCommitmentFormat) === FeeratePerKw(2500 sat)) + assert(spec.htlcTxFeerate(Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === FeeratePerKw(2500 sat)) + assert(spec.htlcTxFeerate(Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) === FeeratePerKw(0 sat)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index 4c5808ecfa..67e7fe4242 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -36,11 +36,9 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { // @formatter:off def filename: String def channelFeatures: ChannelFeatures + val commitmentFormat = channelFeatures.commitmentFormat // @formatter:on - val channelType = channelFeatures.channelType - val commitmentFormat = channelType.commitmentFormat - val tests = { val tests = collection.mutable.HashMap.empty[String, Map[String, String]] val current = collection.mutable.HashMap.empty[String, String] @@ -197,7 +195,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { localFundingPubkey = Local.funding_pubkey, remoteFundingPubkey = Remote.funding_pubkey, spec, - channelType) + commitmentFormat) val commitTx = { val tx = Transactions.makeCommitTx( @@ -214,7 +212,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { Transactions.addSigs(tx, Local.funding_pubkey, Remote.funding_pubkey, local_sig, remote_sig) } - val baseFee = Transactions.commitTxFeeMsat(Local.dustLimit, spec, channelType) + val baseFee = Transactions.commitTxFeeMsat(Local.dustLimit, spec, commitmentFormat) logger.info(s"# base commitment transaction fee = ${baseFee.toLong}") val actualFee = fundingAmount - commitTx.tx.txOut.map(_.amount).sum logger.info(s"# actual commitment transaction fee = ${actualFee.toLong}") @@ -237,7 +235,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { Local.dustLimit, Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey, - spec.htlcTxFeerate(channelType), + spec.htlcTxFeerate(commitmentFormat), outputs, commitmentFormat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 6efbde534f..59b747503a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -21,7 +21,6 @@ import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} import fr.acinq.bitcoin.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, SIGHASH_ALL, SIGHASH_ANYONECANPAY, SIGHASH_NONE, SIGHASH_SINGLE, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, millibtc2satoshi} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.ChannelTypes import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc} import fr.acinq.eclair.transactions.Scripts.{anchor, htlcOffered, htlcReceived, toLocalDelayed} @@ -105,7 +104,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 800000 msat, ByteVector32.Zeroes, CltvExpiry(551), TestConstants.emptyOnionPacket)) ) val spec = CommitmentSpec(htlcs, FeeratePerKw(5000 sat), toLocal = 0 msat, toRemote = 0 msat) - val fee = commitTxFeeMsat(546 sat, spec, ChannelTypes.Standard) + val fee = commitTxFeeMsat(546 sat, spec, DefaultCommitmentFormat) assert(fee === 5340000.msat) } @@ -169,7 +168,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val paymentPreimage = randomBytes32() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val spec = CommitmentSpec(Set(OutgoingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.Standard) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), DefaultCommitmentFormat))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) @@ -184,7 +183,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val paymentPreimage = randomBytes32() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), toLocalDelay.toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val spec = CommitmentSpec(Set(IncomingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.Standard) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry, DefaultCommitmentFormat))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(claimClaimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) @@ -215,37 +214,37 @@ class TransactionsSpec extends AnyFunSuite with Logging { test("generate valid commitment with some outputs that don't materialize (default commitment format)") { val spec = CommitmentSpec(htlcs = Set.empty, commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) - val commitFee = commitTxTotalCost(localDustLimit, spec, ChannelTypes.Standard) + val commitFee = commitTxTotalCost(localDustLimit, spec, DefaultCommitmentFormat) val belowDust = (localDustLimit * 0.9).toMilliSatoshi val belowDustWithFee = (localDustLimit + commitFee * 0.9).toMilliSatoshi { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, ChannelTypes.Standard) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, DefaultCommitmentFormat) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToLocal)) assert(outputs.head.output.amount.toMilliSatoshi === toRemoteFundeeBelowDust.toLocal - commitFee) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFee) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, ChannelTypes.Standard) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, DefaultCommitmentFormat) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToRemote)) assert(outputs.head.output.amount.toMilliSatoshi === toLocalFunderBelowDust.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFee) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, ChannelTypes.Standard) + val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, DefaultCommitmentFormat) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToLocal)) assert(outputs.head.output.amount.toMilliSatoshi === toRemoteFunderBelowDust.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, ChannelTypes.Standard) + val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, DefaultCommitmentFormat) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToRemote)) assert(outputs.head.output.amount.toMilliSatoshi === toLocalFundeeBelowDust.toRemote - commitFee) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, ChannelTypes.Standard) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, DefaultCommitmentFormat) assert(outputs.isEmpty) } } @@ -280,7 +279,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.Standard) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val commitTxNumber = 0x404142434445L val commitTx = { @@ -298,7 +297,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert((check ^ num) == commitTxNumber) } - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ChannelTypes.Standard), outputs, DefaultCommitmentFormat) + val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(DefaultCommitmentFormat), outputs, DefaultCommitmentFormat) assert(htlcTxs.length === 4) val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } @@ -432,12 +431,12 @@ class TransactionsSpec extends AnyFunSuite with Logging { test("generate valid commitment with some outputs that don't materialize (anchor outputs)") { val spec = CommitmentSpec(htlcs = Set.empty, commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) - val commitFeeAndAnchorCost = commitTxTotalCost(localDustLimit, spec, ChannelTypes.AnchorOutputs) + val commitFeeAndAnchorCost = commitTxTotalCost(localDustLimit, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) val belowDust = (localDustLimit * 0.9).toMilliSatoshi val belowDustWithFeeAndAnchors = (localDustLimit + commitFeeAndAnchorCost * 0.9).toMilliSatoshi { - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.AnchorOutputs) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToRemote, CommitmentOutput.ToLocalAnchor, CommitmentOutput.ToRemoteAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) @@ -446,35 +445,35 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, ChannelTypes.AnchorOutputs) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFeeAndAnchorCost) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFeeAndAnchors) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, ChannelTypes.AnchorOutputs) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFeeAndAnchors) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, ChannelTypes.AnchorOutputs) + val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, ChannelTypes.AnchorOutputs) + val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote - commitFeeAndAnchorCost) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, ChannelTypes.AnchorOutputs) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.isEmpty) } } @@ -491,9 +490,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { val htlc2b = UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliBtc(150).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(310), TestConstants.emptyOnionPacket) // htlc3 and htlc4 are dust IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage val paymentPreimage3 = randomBytes32() - val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, AnchorOutputsCommitmentFormat.htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(295), TestConstants.emptyOnionPacket) + val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat.htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(295), TestConstants.emptyOnionPacket) val paymentPreimage4 = randomBytes32() - val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit + weight2fee(feeratePerKw, AnchorOutputsCommitmentFormat.htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), CltvExpiry(300), TestConstants.emptyOnionPacket) + val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit + weight2fee(feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat.htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), CltvExpiry(300), TestConstants.emptyOnionPacket) // htlc5 and htlc6 are dust IN/OUT htlcs val htlc5 = UpdateAddHtlc(ByteVector32.Zeroes, 5, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(295), TestConstants.emptyOnionPacket) val htlc6 = UpdateAddHtlc(ByteVector32.Zeroes, 6, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(305), TestConstants.emptyOnionPacket) @@ -518,13 +517,13 @@ class TransactionsSpec extends AnyFunSuite with Logging { val (commitTx, commitTxOutputs, htlcTimeoutTxs, htlcSuccessTxs) = { val commitTxNumber = 0x404142434445L - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.AnchorOutputs) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) - val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) - val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, AnchorOutputsCommitmentFormat) + val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat) val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ChannelTypes.AnchorOutputs), outputs, AnchorOutputsCommitmentFormat) + val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(UnsafeLegacyAnchorOutputsCommitmentFormat), outputs, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(htlcTxs.length === 5) val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } @@ -533,9 +532,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(htlcSuccessTxs.size == 3) // htlc2a, htlc2b and htlc4 assert(htlcSuccessTxs.map(_.htlcId).toSet === Set(1, 2, 4)) - val zeroFeeOutputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + val zeroFeeOutputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) val zeroFeeCommitTx = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, zeroFeeOutputs) - val zeroFeeHtlcTxs = makeHtlcTxs(zeroFeeCommitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ChannelTypes.AnchorOutputsZeroFeeHtlcTx), zeroFeeOutputs, AnchorOutputsCommitmentFormat) + val zeroFeeHtlcTxs = makeHtlcTxs(zeroFeeCommitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ZeroFeeHtlcTxAnchorOutputsCommitmentFormat), zeroFeeOutputs, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) assert(zeroFeeHtlcTxs.length === 7) val zeroFeeHtlcSuccessTxs = zeroFeeHtlcTxs.collect { case tx: HtlcSuccessTx => tx } val zeroFeeHtlcTimeoutTxs = zeroFeeHtlcTxs.collect { case tx: HtlcTimeoutTx => tx } @@ -552,7 +551,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends main delayed output val Right(claimMainOutputTx) = makeClaimLocalDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimMainOutputTx, localDelayedPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val localSig = sign(claimMainOutputTx, localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signedTx = addSigs(claimMainOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -564,7 +563,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends main delayed output val Right(claimRemoteDelayedOutputTx) = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(claimRemoteDelayedOutputTx, remotePaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val localSig = sign(claimRemoteDelayedOutputTx, remotePaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signedTx = addSigs(claimRemoteDelayedOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -572,7 +571,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // local spends local anchor val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, localFundingPriv.publicKey) assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = sign(claimAnchorOutputTx, localFundingPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val localSig = sign(claimAnchorOutputTx, localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signedTx = addSigs(claimAnchorOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -580,29 +579,29 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends remote anchor val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, remoteFundingPriv.publicKey) assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = sign(claimAnchorOutputTx, remoteFundingPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val localSig = sign(claimAnchorOutputTx, remoteFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signedTx = addSigs(claimAnchorOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends local main delayed output with revocation key val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw) - val sig = sign(mainPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val sig = sign(mainPenaltyTx, localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signed = addSigs(mainPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } { // local spends received htlc with HTLC-timeout tx for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = sign(htlcTimeoutTx, localHtlcPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) - val remoteSig = sign(htlcTimeoutTx, remoteHtlcPriv, TxOwner.Remote, AnchorOutputsCommitmentFormat) - val signedTx = addSigs(htlcTimeoutTx, localSig, remoteSig, AnchorOutputsCommitmentFormat) + val localSig = sign(htlcTimeoutTx, localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val remoteSig = sign(htlcTimeoutTx, remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat) + val signedTx = addSigs(htlcTimeoutTx, localSig, remoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) // local detects when remote doesn't use the right sighash flags val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) for (sighash <- invalidSighash) { val invalidRemoteSig = sign(htlcTimeoutTx, remoteHtlcPriv, sighash) - val invalidTx = addSigs(htlcTimeoutTx, localSig, invalidRemoteSig, AnchorOutputsCommitmentFormat) + val invalidTx = addSigs(htlcTimeoutTx, localSig, invalidRemoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(invalidTx).isFailure) } } @@ -610,7 +609,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends delayed output of htlc1 timeout tx val Right(htlcDelayed) = makeHtlcDelayedTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = sign(htlcDelayed, localDelayedPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val localSig = sign(htlcDelayed, localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit @@ -620,19 +619,19 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends offered htlc with HTLC-success tx for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(0), paymentPreimage4) :: (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(2), paymentPreimage2) :: Nil) { - val localSig = sign(htlcSuccessTx, localHtlcPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) - val remoteSig = sign(htlcSuccessTx, remoteHtlcPriv, TxOwner.Remote, AnchorOutputsCommitmentFormat) - val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, AnchorOutputsCommitmentFormat) + val localSig = sign(htlcSuccessTx, localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val remoteSig = sign(htlcSuccessTx, remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat) + val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) // check remote sig - assert(checkSig(htlcSuccessTx, remoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, AnchorOutputsCommitmentFormat)) + assert(checkSig(htlcSuccessTx, remoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat)) // local detects when remote doesn't use the right sighash flags val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) for (sighash <- invalidSighash) { val invalidRemoteSig = sign(htlcSuccessTx, remoteHtlcPriv, sighash) - val invalidTx = addSigs(htlcSuccessTx, localSig, invalidRemoteSig, paymentPreimage, AnchorOutputsCommitmentFormat) + val invalidTx = addSigs(htlcSuccessTx, localSig, invalidRemoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(invalidTx).isFailure) - assert(!checkSig(invalidTx, invalidRemoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, AnchorOutputsCommitmentFormat)) + assert(!checkSig(invalidTx, invalidRemoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat)) } } } @@ -641,7 +640,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Right(htlcDelayedA) = makeHtlcDelayedTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val Right(htlcDelayedB) = makeHtlcDelayedTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (htlcDelayed <- Seq(htlcDelayedA, htlcDelayedB)) { - val localSig = sign(htlcDelayed, localDelayedPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val localSig = sign(htlcDelayed, localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -652,8 +651,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends local->remote htlc outputs directly in case of success for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { - val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, AnchorOutputsCommitmentFormat) - val localSig = sign(claimHtlcSuccessTx, remoteHtlcPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = sign(claimHtlcSuccessTx, remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) assert(checkSpendable(signed).isSuccess) } @@ -661,7 +660,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends htlc1's htlc-timeout tx with revocation key val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit @@ -671,8 +670,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends remote->local htlc output directly in case of timeout for (htlc <- Seq(htlc2a, htlc2b)) { - val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, AnchorOutputsCommitmentFormat) - val localSig = sign(claimHtlcTimeoutTx, remoteHtlcPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = sign(claimHtlcTimeoutTx, remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signed = addSigs(claimHtlcTimeoutTx, localSig) assert(checkSpendable(signed).isSuccess) } @@ -682,7 +681,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Seq(Right(claimHtlcDelayedPenaltyTxA)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val Seq(Right(claimHtlcDelayedPenaltyTxB)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (claimHtlcSuccessPenaltyTx <- Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB)) { - val sig = sign(claimHtlcSuccessPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val sig = sign(claimHtlcSuccessPenaltyTx, localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signed = addSigs(claimHtlcSuccessPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } @@ -706,26 +705,26 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // remote spends offered htlc output with revocation key - val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash), AnchorOutputsCommitmentFormat)) + val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash), UnsafeLegacyAnchorOutputsCommitmentFormat)) val Some(htlcOutputIndex) = commitTxOutputs.zipWithIndex.find { case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(someHtlc))), _) => someHtlc.id == htlc1.id case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = sign(htlcPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val sig = sign(htlcPenaltyTx, localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } { // remote spends received htlc output with revocation key for (htlc <- Seq(htlc2a, htlc2b)) { - val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc.paymentHash), htlc.cltvExpiry, AnchorOutputsCommitmentFormat)) + val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc.paymentHash), htlc.cltvExpiry, UnsafeLegacyAnchorOutputsCommitmentFormat)) val Some(htlcOutputIndex) = commitTxOutputs.zipWithIndex.find { case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(someHtlc))), _) => someHtlc.id == htlc.id case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = sign(htlcPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) + val sig = sign(htlcPenaltyTx, localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -769,7 +768,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTxNumber = 0x404142434446L val (commitTx, outputs, htlcTxs) = { - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ChannelTypes.Standard) + val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat) @@ -896,7 +895,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(tests.size === 30, "there were 15 tests at b201efe0546120c14bf154ce5f4e18da7243fe7a") // simple non-reg to make sure we are not missing tests tests.foreach(test => { logger.info(s"running BOLT 3 test: '${test.name}'") - val fee = commitTxTotalCost(dustLimit, test.spec, ChannelTypes.Standard) + val fee = commitTxTotalCost(dustLimit, test.spec, DefaultCommitmentFormat) assert(fee === test.expectedFee) }) }