diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index c58c81d0c4..742868eda5 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -50,6 +50,7 @@ eclair { basic_mpp = optional option_support_large_channel = optional 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..d99b46cc5b 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} @@ -33,7 +32,7 @@ case class ChannelFeatures(activated: Set[Feature]) { /** Format of the channel transactions. */ val commitmentFormat: CommitmentFormat = { - if (hasFeature(AnchorOutputs)) { + if (hasFeature(Features.AnchorOutputs) || hasFeature(Features.AnchorOutputsZeroFeeHtlcTxs)) { AnchorOutputsCommitmentFormat } else { DefaultCommitmentFormat @@ -41,9 +40,11 @@ case class ChannelFeatures(activated: Set[Feature]) { } 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 @@ -66,7 +67,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: _*) } @@ -102,6 +103,11 @@ object ChannelTypes { override def paysDirectlyToWallet: Boolean = false 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 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 +116,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 +125,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 af344ab4cc..d1568ac904 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 c3e7448bf1..d189402f13 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 dae6e99b70..1fabb6dcf7 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 9969d4878b..5aa90c47dd 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 0105cfad47..5f60a8720c 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,"messageFlags":1,"channelFlags":1,"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,"messageFlags":1,"channelFlags":1,"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,"messageFlags":1,"channelFlags":1,"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,"messageFlags":1,"channelFlags":1,"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 c422603007..210f468cf1 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 @@ -18,8 +18,6 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong} -import fr.acinq.eclair.FeatureSupport.Mandatory -import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelTypes @@ -168,10 +166,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 f71f7f3e00..c74c6b8bab 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 10a6730f6f..6a50adaa87 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 @@ -305,6 +305,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 channelIds and shortChannelIds") {